Skip to content

Conversation

@johnzhou721
Copy link

@johnzhou721 johnzhou721 commented Apr 24, 2025

Refs #7 and #5, with #7 being the messed-up version of this patch. This is still draft. Part of beeware/Python-Apple-support#117

@johnzhou721
Copy link
Author

johnzhou721 commented Apr 24, 2025

OK so command I'm testing locally w/ now (the cflags and ldflags don't worry they are my own directories w/ unpacked deps packages

export PATH=./MacCatalyst/Resources/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin
./configure --host=arm64-apple-ios14.0-macabi --build=arm64-apple-darwin --enable-framework --with-build-python=/Library/Frameworks/Python.framework/Versions/3.14/bin/python3.14 CFLAGS="-I/Users/johnzhou/cpython-macabi-deps/include" LDFLAGS="-L/Users/johnzhou/cpython-macabi-deps/lib" LIBLZMA_LIBS="-llzma" BZIP2_LIBS="-lbz2" LIBMPDEC_LIBS="-lmpdec" LIBFFI_LIBS="-lffi" --with-openssl=/Users/johnzhou/cpython-macabi-deps/ && make -j16 && make install

This is just for my own benefit.

@johnzhou721
Copy link
Author

A way more involved set of changes is required; Mac Catalyst is versioned.

@johnzhou721
Copy link
Author

Need logic to get the MACOS version for this -- currently we have ios deployment target:

<key>LSMinimumSystemVersion</key>
	<string>10.0</string>

@johnzhou721
Copy link
Author

OK just destroyed the iOS testbed -- Mac Catalyst has a sort of different process to do stuff (not using simulators, diff ways to codesign etc) and I'm not that familiar and will investigate later.

@johnzhou721
Copy link
Author

More stuff also needs to be modified w.r.t. versioned structure (but embedded only) for Mac Catalyst.

@johnzhou721
Copy link
Author

Just pushed 2 nonworking commits on the versioned structure for mac catalyst but I have absolutely no clue right now... so I need to dig in a bit

Also apologize for the large amounts of small comments.

@freakboy3742 freakboy3742 force-pushed the 3.14-patched branch 2 times, most recently from 50c666f to 0dbdbcd Compare April 25, 2025 03:31
@freakboy3742
Copy link
Owner

This branch has now diverged pretty far from the current state of 3.14-patched; I've got all these changes collapsed into a single commit if you'd like me to force-push an update to this branch.

In my experimentation, the build completes, but testing isn't a simple process. The testbed shouldn't be needed, as there's no need to start a simulator... but python.exe doesn't work either, because of RPATH-based framework linking issues.

@johnzhou721
Copy link
Author

johnzhou721 commented Apr 25, 2025 via email

@johnzhou721
Copy link
Author

OK just finished force-pushing -- added some new changes.

@johnzhou721
Copy link
Author

Right now we have

ln -fs "../../../Python" "iOS/Frameworks/arm64-iphoneos-macabi/Python.framework/Versions/3.14/lib/python3.14/config-3.14-arm64-iphoneos-macabi/libpython3.14.a"
ln: iOS/Frameworks/arm64-iphoneos-macabi/Python.framework/Versions/3.14/lib/python3.14/config-3.14-arm64-iphoneos-macabi/libpython3.14.a: No such file or directory
make: *** [frameworkinstallmaclib] Error 1

@johnzhou721
Copy link
Author

OK that is fixed and now this builds!

rpath needs to be fixed definitely -- but I think we should start the review process.

Remarks:

  1. The plist generated as of 2025 now uses the macOS version instead of the iOS version in the minimum deployment target for MacCatalyst, we need to find a mapping of these values and hardcode them -- https://stackoverflow.com/questions/63581114/mac-catalyst-version may be helpful but I'll save the polishing until later -- we need the embedding done first.
  2. When we run Python.exe now in the terminal, what is failing is NOT the rpath (although it is problematic, see later) but some stuff during startup using _ios_support and somehow getting a none somewhere -- also ios platform isn't really accomodated for terminal stuff I guess so this might require changes, but just for testing purposes -- but I'd rather have an ``embedded''/testbed test because then we test how it actually works in an application and whether any of the embedding process is broken.
  3. rpath issue. Right now it referenced the path of the framework but within the build directory, which is problematic. Will investigate later.
  4. Testing -- To [provide a testbed] or not to [provide a testbed], that is the question. My inclination is yes -- see two items abover; however we shall figure out how to put stuff in the bundle etc.

Overall I'm ending my day now but will make this ready for review so you might leave some additional remarks, even though it'll probably take at least 1-2 weeks on my end to get this merged, because I have school mon-fri and (just like you, sorry for not respecting that and the repeated pings) spend time w/ family (aka. mom dad and sibling(s) and those only) on sat and sun, so I might only have 1h at worst per day.

@johnzhou721 johnzhou721 marked this pull request as ready for review April 26, 2025 03:04
@johnzhou721
Copy link
Author

Oh I'm really sorry but forgot to add

5. Platform identitification -- how can we know we're on Catalyst? Any stuff to take care of regarding iOS vs Catalyst?

Also that said I'll be waking up very early tomorrow so like if you have urgent questions you can ask 6:20 pm in Perth, Australia (it's 5:20am here, but I have some stuff at 5:30am which I won't disclose).

@freakboy3742
Copy link
Owner

OK that is fixed and now this builds!

rpath needs to be fixed definitely -- but I think we should start the review process.

So - it's very difficult to review something that doesn't actually do anything yet. I can pick on cosmetic stuff (the indentation in the configure.ac is the biggest thing that stands out); but unless I can start an interpreter or run the test suite... it doesn't work yet. There's nothing to meaningfully review.

This PR doesn't contain a testbed project; and as I said yesterday, when I run python.exe from any location, or with any DYLD_LIBRARY_PATH set, I get RPATH errors... so if you're getting further than that, you'll need to reveal your testing strategy.

@johnzhou721
Copy link
Author

@freakboy3742 Thanks for organizing everything in the latest PR but afraid would have to revert some changes in the configure.ac -- anyway is a separate testbed acceptable? Or would you prefer it to be one? The latter would require some stuff and the glue scripts to be re-written.

@johnzhou721
Copy link
Author

Will bundle a testbed soon but in the meantime a humongous amount of tests are failing on my end.

@freakboy3742
Copy link
Owner

@freakboy3742 Thanks for organizing everything in the latest PR but afraid would have to revert some changes in the configure.ac -- anyway is a separate testbed acceptable? Or would you prefer it to be one? The latter would require some stuff and the glue scripts to be re-written.

I'm not clear why you've reverted the configure.ac changes though. Why does MacCatalyst require a versioned framework? A versioned framework is required on macOS because it's installed in /Library; that won't be the case here.

As for the testbed; I've been considering whether the iOS, tvOS, watchOS, visionOS and MacCatalyst directories need to be merged into a single Apple folder so that they can share a common testbed project and script, with multiple output targets. We don't need to solve that problem right now, but it's something that probably needs to be done sooner rather than later. For right now, I'm OK with a duplicated testbed project.

@freakboy3742
Copy link
Owner

Also - as there have been changes to the visionOS patch, this PR now needs to be updated; when you do, you need to use a rebase, not a merge commit. The "patch tree" approach to managing the 3.14-patched branch works isn't compatible with merge commits.

@johnzhou721
Copy link
Author

I'm not clear why you've reverted the configure.ac changes though. Why does MacCatalyst require a versioned framework? A versioned framework is required on macOS because it's installed in /Library; that won't be the case here.

macOS distribution requires versioned framework AFAIK; see, for example, the Frameworks folder of LyX, a WYSIWYM LaTeX editor, and things like QtCore.framework is versioned:

Screenshot 2025-04-27 at 10 27 28 AM

@johnzhou721
Copy link
Author

@freakboy3742 Done

@johnzhou721
Copy link
Author

johnzhou721 commented Apr 27, 2025

There's no way AFAIK to obtain the macOS version from the Mac Catalyst verison, the former of which is actually requried in our plist, so I provided the correct value for the default version and added --with-catalyst-macos-version to pass in the macOS version.

@johnzhou721
Copy link
Author

I just killed the MacCatalyst testbed and merged with iOS testbed. Let me know what you think -- b/c this is usually what they do when they build actual apps.

Copy link
Owner

@freakboy3742 freakboy3742 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not clear why you've reverted the configure.ac changes though. Why does MacCatalyst require a versioned framework? A versioned framework is required on macOS because it's installed in /Library; that won't be the case here.

macOS distribution requires versioned framework AFAIK; see, for example, the Frameworks folder of LyX, a WYSIWYM LaTeX editor, and things like QtCore.framework is versioned:

I'm not arguing that you can't use a versioned framework - I'm asking if we must use a versioned framework.

I just killed the MacCatalyst testbed and merged with iOS testbed. Let me know what you think -- b/c this is usually what they do when they build actual apps.

Some notes inline; keep in mind that one of the primary goals here is that this is a patch on top of cpython, so changes must be kept to a minimium. Even "minor reorganisations" are a problem because they won't merge/rebase cleanly.

There's no way AFAIK to obtain the macOS version from the Mac Catalyst verison, the former of which is actually requried in our plist, so I provided the correct value for the default version and added --with-catalyst-macos-version to pass in the macOS version.

Why is the macOS version needed? macOS version matters when it's a gateway for features... but if you're on Catalyst, the macOS version isn't the gateway being used - it's the iOS version.

@johnzhou721
Copy link
Author

Turns out it was caused by me, myself. https://en.wikipedia.org/wiki/Memory_leak#Pseudocode is pretty similar, not sure if this is it, will retest

@johnzhou721
Copy link
Author

Yes!!! For the first time in forever, I got a clean testbed run.

Requesting a review in GitHub again, but the existing things I have not resolved are still pending reply.

Thank you for your extremely nontrivial assistance on this patch.

@johnzhou721 johnzhou721 requested a review from freakboy3742 May 18, 2025 01:17
@freakboy3742
Copy link
Owner

FYI - I likely won't get a chance to look at this for a couple of days; PyCon US sprints for the next couple of days will keep me fairly well occupied.

@johnzhou721
Copy link
Author

johnzhou721 commented Jun 3, 2025

@freakboy3742 Don't believe the force push is correct... will need to drop the tvOS, watchOS + visionOS commits.

EDIT or is this intentional to combine into 1 commit?

EDIT nevermind it's me who need to rebase... misread branch name, sorry

@johnzhou721
Copy link
Author

Done. Really sorry for accientally misreading the branch name in the last comment.

miss-islington and others added 3 commits June 4, 2025 10:36
…on't use `simctl --set testing` (pythonGH-135102) (python#135113)

On a fresh Xcode install (including some CI provider configurations), there is
no pre-existing testing set that can be used to identify simulator models. Use
the default device set to detect available models instead. Live testing
simulators are still created in the testing set.
(cherry picked from commit dba9de7)

Co-authored-by: Joe Rickerby <[email protected]>
@johnzhou721
Copy link
Author

Aplogies for missing the latest force push.

commit a228887
Author: John Zhou <[email protected]>
Date:   Sat May 17 19:53:30 2025 -0500

    https://en.wikipedia.org/wiki/Memory_leak#Pseudocode

commit 78eb704
Author: John Zhou <[email protected]>
Date:   Thu May 15 22:12:29 2025 -0500

    Revert "try to build multiprocessing"

    This reverts commit 4e92456.

commit 687916e
Author: John Zhou <[email protected]>
Date:   Thu May 15 22:07:48 2025 -0500

    try to build multiprocessing

commit 02be1ef
Author: John Zhou <[email protected]>
Date:   Thu May 15 18:31:47 2025 -0500

    misc fixes

commit 1ff1574
Author: John Zhou <[email protected]>
Date:   Thu May 15 17:14:16 2025 -0500

    why do i forget to handle exceptions???

commit ff71f93
Author: John Zhou <[email protected]>
Date:   Thu May 15 16:54:10 2025 -0500

    grumble

commit 8d3f831
Author: John Zhou <[email protected]>
Date:   Wed May 14 20:23:37 2025 -0500

    grumble

commit 5d73cfc
Author: John Zhou <[email protected]>
Date:   Wed May 14 20:17:51 2025 -0500

    fix test_os

commit 084a83c
Author: John Zhou <[email protected]>
Date:   Sat May 10 21:50:13 2025 -0500

    disable sandbox

commit c8227e4
Author: John Zhou <[email protected]>
Date:   Thu May 8 22:07:42 2025 -0500

    platform fix

commit 00c08c3
Author: John Zhou <[email protected]>
Date:   Thu May 8 21:28:54 2025 -0500

    fix platform on tvOS et. al.

commit 63433a6
Author: John Zhou <[email protected]>
Date:   Mon May 5 20:57:35 2025 -0500

    add a missing file

commit 9930836
Author: John Zhou <[email protected]>
Date:   Sat May 10 07:42:54 2025 -0500

    use python for build

commit 1ee48cd
Author: John Zhou <[email protected]>
Date:   Sat May 3 19:23:52 2025 -0500

    install binaries

commit 19e8d64
Author: John Zhou <[email protected]>
Date:   Sat May 3 14:39:59 2025 -0500

    merge test commands

commit 639bf33
Author: John Zhou <[email protected]>
Date:   Sat May 3 09:02:19 2025 -0500

    elif

commit f1b9686
Author: John Zhou <[email protected]>
Date:   Sat May 3 09:00:09 2025 -0500

    another fix

commit 7253cdd
Author: John Zhou <[email protected]>
Date:   Sat May 3 08:53:54 2025 -0500

    fix test

commit ab9480e
Author: John Zhou <[email protected]>
Date:   Fri May 2 20:54:59 2025 -0500

    grumble

commit 5ec2564
Author: John Zhou <[email protected]>
Date:   Fri May 2 20:50:18 2025 -0500

    nvm that did not work

commit 59e62e6
Author: John Zhou <[email protected]>
Date:   Fri May 2 20:34:26 2025 -0500

    more fixes, also parallel

commit da68fb6
Author: John Zhou <[email protected]>
Date:   Fri May 2 18:49:29 2025 -0500

    remove irrel comment

commit d662098
Author: John <[email protected]>
Date:   Fri May 2 18:44:00 2025 -0500

    Update test_util.py

commit 06e8510
Author: John <[email protected]>
Date:   Fri May 2 18:43:24 2025 -0500

    Update test_loader.py

commit daf3275
Author: John <[email protected]>
Date:   Fri May 2 18:42:41 2025 -0500

    Update __init__.py

commit aaf27df
Author: John <[email protected]>
Date:   Fri May 2 18:41:56 2025 -0500

    Update test_misc.py

commit 1588275
Author: John <[email protected]>
Date:   Fri May 2 18:41:24 2025 -0500

    Update datetimetester.py

commit fc008b8
Author: John Zhou <[email protected]>
Date:   Fri May 2 18:44:37 2025 -0500

    stuff

commit da006cc
Author: John Zhou <[email protected]>
Date:   Fri May 2 18:40:58 2025 -0500

    add flag for fwork

commit ad7a0e1
Author: John Zhou <[email protected]>
Date:   Fri May 2 18:34:53 2025 -0500

    haaaands

commit a29e718
Author: John Zhou <[email protected]>
Date:   Fri May 2 18:26:38 2025 -0500

    yet another error

commit 8b41226
Author: John Zhou <[email protected]>
Date:   Fri May 2 18:08:23 2025 -0500

    outdated message

commit 6c07c00
Author: John Zhou <[email protected]>
Date:   Thu May 1 21:49:34 2025 -0500

    more fixups

commit 897e182
Author: John Zhou <[email protected]>
Date:   Thu May 1 20:58:06 2025 -0500

    another ref

commit 5edde3a
Author: John Zhou <[email protected]>
Date:   Tue Apr 29 20:12:19 2025 -0500

    whitespace

commit 3ece1d6
Author: John Zhou <[email protected]>
Date:   Tue Apr 29 18:11:42 2025 -0500

    I'm not sure if multiprocessing is available...

commit c47a04b
Author: John Zhou <[email protected]>
Date:   Tue Apr 29 17:39:16 2025 -0500

    enable testing stuff

commit fecd3b5
Author: John Zhou <[email protected]>
Date:   Tue Apr 29 17:23:02 2025 -0500

    support fork, fix another appleframeworkloader reference

commit 834b207
Author: John Zhou <[email protected]>
Date:   Tue Apr 29 16:52:59 2025 -0500

    detect mac catalyst in datetimetester

commit 03a40b1
Author: John Zhou <[email protected]>
Date:   Tue Apr 29 07:47:53 2025 -0500

    use sys

commit 849e276
Author: John Zhou <[email protected]>
Date:   Tue Apr 29 07:28:55 2025 -0500

    bootstrap external cleanup`

commit 65e3bf8
Author: John Zhou <[email protected]>
Date:   Tue Apr 29 07:20:31 2025 -0500

    (untested) add mac catalyst detection

commit 8e008c4
Author: John Zhou <[email protected]>
Date:   Mon Apr 28 21:37:37 2025 -0500

    minor adjustments to test script

commit 3d539a4
Author: John Zhou <[email protected]>
Date:   Mon Apr 28 21:30:45 2025 -0500

    disable lib valid + use ad hoc

commit d16af17
Author: John Zhou <[email protected]>
Date:   Sun Apr 27 21:49:08 2025 -0500

    change the regex for catalyst and add if

commit 823d7b6
Author: John Zhou <[email protected]>
Date:   Sun Apr 27 21:43:57 2025 -0500

    add missing entitlemnet

commit 5604df8
Author: John Zhou <[email protected]>
Date:   Sun Apr 27 21:32:21 2025 -0500

    whitespace

commit 5f767fd
Author: John Zhou <[email protected]>
Date:   Tue Jun 3 08:12:12 2025 -0500

    glue for testbed

commit e7b83ae
Author: John Zhou <[email protected]>
Date:   Sun Apr 27 18:11:17 2025 -0500

    fix plist

commit de0ab4d
Author: John Zhou <[email protected]>
Date:   Sun Apr 27 18:09:32 2025 -0500

    address review

commit 87482cd
Author: John <[email protected]>
Date:   Sun Apr 27 14:53:52 2025 -0500

    Delete MacCatalyst/Resources/pyconfig.h

commit 2e971be
Author: John Zhou <[email protected]>
Date:   Sun Apr 27 12:27:10 2025 -0500

    plist adjustments + kill maccatalyst testbed

    revert changes

commit 8d186e3
Author: John Zhou <[email protected]>
Date:   Sun Apr 27 11:40:04 2025 -0500

    esac

commit 1a20cd7
Author: John Zhou <[email protected]>
Date:   Sun Apr 27 11:38:09 2025 -0500

    fixes for plist

commit f00cadf
Author: John Zhou <[email protected]>
Date:   Sun Apr 27 10:47:38 2025 -0500

    git rebase cleanup

commit 956cfbd
Author: John Zhou <[email protected]>
Date:   Sat Apr 26 07:00:24 2025 -0500

    Remove ignored files from tracking

commit 3828ce4
Author: John Zhou <[email protected]>
Date:   Sat Apr 26 07:00:09 2025 -0500

    remove more ios refs

commit bd99ef5
Author: John Zhou <[email protected]>
Date:   Sat Apr 26 06:49:55 2025 -0500

    testbed

commit 2479a35
Author: John Zhou <[email protected]>
Date:   Sat Apr 26 06:11:51 2025 -0500

    here more code

commit b3adac4
Author: John Zhou <[email protected]>
Date:   Sat Apr 26 05:50:09 2025 -0500

    fix

commit ce636cd
Author: John Zhou <[email protected]>
Date:   Sat Apr 26 05:47:45 2025 -0500

    rpath fix w/ embedded

commit e0ec373
Author: John Zhou <[email protected]>
Date:   Sun Apr 27 10:46:32 2025 -0500

    Minor cleanups.

commit 4071f1d
Author: John Zhou <[email protected]>
Date:   Fri Apr 25 20:36:44 2025 -0500

    remove useless stuff.

commit 54856e3
Author: John Zhou <[email protected]>
Date:   Fri Apr 25 18:48:41 2025 -0500

    another fix

commit 97598c4
Author: John Zhou <[email protected]>
Date:   Fri Apr 25 18:43:42 2025 -0500

    another fix

commit f39ed64
Author: John Zhou <[email protected]>
Date:   Fri Apr 25 18:38:12 2025 -0500

    more changes to configure

commit 9de10c0
Author: John Zhou <[email protected]>
Date:   Fri Apr 25 18:30:44 2025 -0500

    Add supp for mac catalyst

    Co-Authored-By: Andrew Savva <[email protected]>
Copy link
Author

@johnzhou721 johnzhou721 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FInal pieces of cleanup.

@johnzhou721
Copy link
Author

FYI: I've cleaned up some of the things I flagged and also cleaned up a bunch of comments.

@johnzhou721
Copy link
Author

Hi @freakboy3742 , been digging through my list of old PRs and found this -- could another look be taken at this?

Current state of this PR should be passing the testbed; most changes have been straightforward except for the guarded file descriptor modification on line 2513 of test_os.py -- fstat fails for guarded FDs making Python think it's open to use, so for mac catalyst (not sure if relevant on other apple platforms?) the test keeps opening new FDs (since they're sequential with the exception of some of those system-guarded ones) on a file until they become non-sequential and uses the sequential range opened to test closerange.

Thanks!

@freakboy3742
Copy link
Owner

Apologies for the delay on reviewing this. I've been mostly deferring looking into this (and #11) because I knew that python#138176 was going to be on my todo list, and it's going to be a lot easier to land other Apple changes once that change is in place.

Actually getting to the point of writing that PR took a lot longer than I was hoping...

So - my current intention is to wait until that PR has landed, and then integrate these changes into that new directory (and build/testbed) structure.

@johnzhou721
Copy link
Author

@freakboy3742 No worries, more of a mistake on my part flooding in new PRs when I haven't completely old ones.

I won't have time to integrate testbed into new directory structure though, so if your schedule's also pretty tight we either omit the testbed part or close this PR. Both options are fine with me.

@johnzhou721
Copy link
Author

Alright. I'm closing this PR since I no longer have time to work on it. I was keeping it open because there weren't any structural changes upstream so I figured someone would just review it, however it seems that I've been starting too much new stuff and has now ran out of time to review the (very good) upstream structural changes..

Diff from 68b83a6 (before upstream structural changes
diff --git a/.gitignore b/.gitignore
index 3893a91e771..4c5221f4f62 100644
--- a/.gitignore
+++ b/.gitignore
@@ -82,6 +82,7 @@ iOS/testbed/Python.xcframework/ios-*/Python.framework
 iOS/testbed/iOSTestbed.xcodeproj/project.xcworkspace
 iOS/testbed/iOSTestbed.xcodeproj/xcuserdata
 iOS/testbed/iOSTestbed.xcodeproj/xcshareddata
+MacCatalyst/Resources/Info.plist
 visionOS/testbed/Python.xcframework/xros-*/bin
 visionOS/testbed/Python.xcframework/xros-*/include
 visionOS/testbed/Python.xcframework/xros-*/lib
diff --git a/Lib/_ios_support.py b/Lib/_ios_support.py
index 20467a7c2bc..deefacaf8c5 100644
--- a/Lib/_ios_support.py
+++ b/Lib/_ios_support.py
@@ -25,6 +25,7 @@
 def get_platform_ios():
     # Determine if this is a simulator using the multiarch value
     is_simulator = sys.implementation._multiarch.endswith("simulator")
+    is_catalyst = sys.implementation._multiarch.endswith("macabi")
 
     # We can't use ctypes; abort
     if not objc:
@@ -68,4 +69,4 @@ def get_platform_ios():
     release = objc.objc_msgSend(device_systemVersion, SEL_UTF8String).decode()
     model = objc.objc_msgSend(device_model, SEL_UTF8String).decode()
 
-    return system, release, model, is_simulator
+    return system, release, model, is_simulator, is_catalyst
diff --git a/Lib/importlib/_bootstrap_external.py b/Lib/importlib/_bootstrap_external.py
index d8a6f28edba..dfab36488f6 100644
--- a/Lib/importlib/_bootstrap_external.py
+++ b/Lib/importlib/_bootstrap_external.py
@@ -1535,7 +1535,8 @@ def _get_supported_file_loaders():
     """
     extension_loaders = []
     if hasattr(_imp, 'create_dynamic'):
-        if sys.platform in {"ios", "tvos", "watchos", "visionos"}:
+        # AppleFrameworkLoader is unessaccary on Mac Catalyst because Mac Catalyst allows "raw" dylib files.
+        if sys.platform in {"ios", "tvos", "watchos", "visionos"} and not sys.implementation._multiarch.endswith("macabi"):
             extension_loaders = [(AppleFrameworkLoader, [
                 suffix.replace(".so", ".fwork")
                 for suffix in _imp.extension_suffixes()
diff --git a/Lib/platform.py b/Lib/platform.py
index cad919bc0c4..37dd6ce7bf1 100644
--- a/Lib/platform.py
+++ b/Lib/platform.py
@@ -508,11 +508,11 @@ def mac_ver(release='', versioninfo=('', '', ''), machine=''):
 # A namedtuple for iOS version information.
 IOSVersionInfo = collections.namedtuple(
     "IOSVersionInfo",
-    ["system", "release", "model", "is_simulator"]
+    ["system", "release", "model", "is_simulator", "is_catalyst"]
 )
 
 
-def ios_ver(system="", release="", model="", is_simulator=False):
+def ios_ver(system="", release="", model="", is_simulator=False, is_catalyst=False):
     """Get iOS version information, and return it as a namedtuple:
         (system, release, model, is_simulator).
 
@@ -525,7 +525,7 @@ def ios_ver(system="", release="", model="", is_simulator=False):
         if result is not None:
             return IOSVersionInfo(*result)
 
-    return IOSVersionInfo(system, release, model, is_simulator)
+    return IOSVersionInfo(system, release, model, is_simulator, is_catalyst)
 
 
 # A namedtuple for tvOS version information.
@@ -547,6 +547,7 @@ def tvos_ver(system="", release="", model="", is_simulator=False):
         import _ios_support
         result = _ios_support.get_platform_ios()
         if result is not None:
+            result = result[:-1]  # ignore the Catalyst flag
             return TVOSVersionInfo(*result)
 
     return TVOSVersionInfo(system, release, model, is_simulator)
@@ -571,6 +572,7 @@ def watchos_ver(system="", release="", model="", is_simulator=False):
         import _ios_support
         result = _ios_support.get_platform_ios()
         if result is not None:
+            result = result[:-1]  # ignore the Catalyst flag
             return WatchOSVersionInfo(*result)
 
     return WatchOSVersionInfo(system, release, model, is_simulator)
@@ -595,6 +597,7 @@ def visionos_ver(system="", release="", model="", is_simulator=False):
         import _ios_support
         result = _ios_support.get_platform_ios()
         if result is not None:
+            result = result[:-1]  # ignore the Catalyst flag
             return VisionOSVersionInfo(*result)
 
     return VisionOSVersionInfo(system, release, model, is_simulator)
@@ -963,12 +966,12 @@ def get_OpenVMS():
             csid, cpu_number = vms_lib.getsyi('SYI$_CPU', 0)
             return 'Alpha' if cpu_number >= 128 else 'VAX'
 
-    # On the iOS/tvOS/watchOS/visionOS simulator, os.uname returns the architecture as
-    # uname.machine. On device it returns the model name for some reason; but
-    # there's only one CPU architecture for devices, so we know the right
-    # answer.
+    # On the iOS/tvOS/watchOS/visionOS simulator and Mac Catalyst, os.uname returns
+    # the architecture as uname.machine. On device it returns the model name for
+    # some reason; but there's only one CPU architecture for devices, so we know the
+    # right answer.
     def get_ios():
-        if sys.implementation._multiarch.endswith("simulator"):
+        if sys.implementation._multiarch.endswith("simulator") or sys.implementation._multiarch.endswith("macabi"):
             return os.uname().machine
         return 'arm64'
 
@@ -1148,7 +1151,7 @@ def uname():
 
     # Normalize responses on Apple mobile platforms
     if sys.platform == 'ios':
-        system, release, _, _ = ios_ver()
+        system, release, _, _, _ = ios_ver()
     if sys.platform == 'tvos':
         system, release, _, _ = tvos_ver()
     if sys.platform == 'watchos':
@@ -1441,9 +1444,9 @@ def platform(aliased=False, terse=False):
         system, release, version = system_alias(system, release, version)
 
     if system == 'Darwin':
-        # macOS and iOS both report as a "Darwin" kernel
+        # All Apple Platforms report as a "Darwin" kernel
         if sys.platform == "ios":
-            system, release, _, _ = ios_ver()
+            system, release, _, _, _ = ios_ver()
         elif sys.platform == "tvos":
             system, release, _, _ = tvos_ver()
         elif sys.platform == "watchos":
diff --git a/Lib/subprocess.py b/Lib/subprocess.py
index 03896a234bf..9b3f9f612e9 100644
--- a/Lib/subprocess.py
+++ b/Lib/subprocess.py
@@ -75,7 +75,10 @@
     _mswindows = True
 
 # some platforms do not support subprocesses
-_can_fork_exec = sys.platform not in {"emscripten", "wasi", "ios", "tvos", "watchos", "visionos"}
+_can_fork_exec = (
+    sys.platform not in {"emscripten", "wasi", "ios", "tvos", "watchos", "visionos"}
+    or sys.implementation._multiarch.endswith("macabi")  # Mac Catalyst
+)
 
 if _mswindows:
     import _winapi
diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py
index 8594f92c097..7eecf926082 100644
--- a/Lib/test/datetimetester.py
+++ b/Lib/test/datetimetester.py
@@ -7159,9 +7159,9 @@ def test_datetime_from_timestamp(self):
                     self.assertEqual(dt_orig, dt_rt)
 
     def test_type_check_in_subinterp(self):
-        # Apple mobile platforms require the use of the custom framework loader,
-        # not the ExtensionFileLoader.
-        if support.is_apple_mobile:
+        # Apple mobile platforms except Mac Catalyst require the use of the
+        # custom framework loader, not the ExtensionFileLoader.
+        if support.needs_apple_fworks:
             extension_loader = "AppleFrameworkLoader"
         else:
             extension_loader = "ExtensionFileLoader"
diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py
index 32243a49e7a..4f027912b0a 100644
--- a/Lib/test/support/__init__.py
+++ b/Lib/test/support/__init__.py
@@ -46,6 +46,7 @@
     # sys
     "MS_WINDOWS", "is_jython", "is_android", "is_emscripten", "is_wasi",
     "is_apple_mobile", "check_impl_detail", "unix_shell", "setswitchinterval",
+    "is_mac_catalyst", "needs_apple_fworks",
     # os
     "get_pagesize",
     # network
@@ -576,6 +577,8 @@ def skip_wasi_stack_overflow():
 
 is_apple_mobile = sys.platform in {"ios", "tvos", "watchos", "visionos"}
 is_apple = is_apple_mobile or sys.platform == "darwin"
+is_mac_catalyst = sys.implementation._multiarch.endswith("macabi")
+needs_apple_fworks = is_apple_mobile and not is_mac_catalyst
 
 has_fork_support = hasattr(os, "fork") and not (
     # WASM and Apple mobile platforms do not support subprocesses.
@@ -586,6 +589,9 @@ def skip_wasi_stack_overflow():
     # Although Android supports fork, it's unsafe to call it from Python because
     # all Android apps are multi-threaded.
     or is_android
+
+    # Mac Catalyst supports subprocesses.
+    and not is_mac_catalyst
 )
 
 def requires_fork():
@@ -601,6 +607,9 @@ def requires_fork():
     # practice (see PEP 738). And most of the tests that use them are calling
     # sys.executable, which won't work when Python is embedded in an Android app.
     or is_android
+
+    # Mac Catalyst supports subprocesses.
+    and not is_mac_catalyst
 )
 
 def requires_subprocess():
diff --git a/Lib/test/test_capi/test_misc.py b/Lib/test/test_capi/test_misc.py
index f74694a7a74..5ad6f4d4ff1 100644
--- a/Lib/test/test_capi/test_misc.py
+++ b/Lib/test/test_capi/test_misc.py
@@ -1920,7 +1920,7 @@ def test_module_state_shared_in_global(self):
 
         # Apple extensions must be distributed as frameworks. This requires
         # a specialist loader.
-        if support.is_apple_mobile:
+        if support.needs_apple_fworks:
             loader = "AppleFrameworkLoader"
         else:
             loader = "ExtensionFileLoader"
@@ -2604,7 +2604,7 @@ def setUp(self):
         origin = importlib.util.find_spec('_testmultiphase').origin
         # Apple extensions must be distributed as frameworks. This requires
         # a specialist loader.
-        if support.is_apple_mobile:
+        if support.needs_apple_fworks:
             loader = importlib.machinery.AppleFrameworkLoader(fullname, origin)
         else:
             loader = importlib.machinery.ExtensionFileLoader(fullname, origin)
diff --git a/Lib/test/test_import/__init__.py b/Lib/test/test_import/__init__.py
index 6e34094c5aa..87a81fdecfd 100644
--- a/Lib/test/test_import/__init__.py
+++ b/Lib/test/test_import/__init__.py
@@ -33,7 +33,7 @@
     swap_attr,
     swap_item,
     cpython_only,
-    is_apple_mobile,
+    needs_apple_fworks,
     is_emscripten,
     is_wasi,
     run_in_subinterp,
@@ -111,7 +111,7 @@ def require_builtin(module, *, skip=False):
 def require_extension(module, *, skip=False):
     # Apple extensions must be distributed as frameworks. This requires
     # a specialist loader.
-    if is_apple_mobile:
+    if needs_apple_fworks:
         _require_loader(module, AppleFrameworkLoader, skip)
     else:
         _require_loader(module, ExtensionFileLoader, skip)
@@ -126,7 +126,7 @@ def require_pure_python(module, *, skip=False):
 def create_extension_loader(modname, filename):
     # Apple extensions must be distributed as frameworks. This requires
     # a specialist loader.
-    if is_apple_mobile:
+    if needs_apple_fworks:
         return AppleFrameworkLoader(modname, filename)
     else:
         return ExtensionFileLoader(modname, filename)
@@ -2217,7 +2217,7 @@ def import_script(self, name, fd, filename=None, check_override=None):
         if filename:
             # Apple extensions must be distributed as frameworks. This requires
             # a specialist loader.
-            if is_apple_mobile:
+            if needs_apple_fworks:
                 loader = "AppleFrameworkLoader"
             else:
                 loader = "ExtensionFileLoader"
@@ -2692,7 +2692,7 @@ def setUpClass(cls):
         # Apple extensions must be distributed as frameworks. This requires
         # a specialist loader, and we need to differentiate between the
         # spec.origin and the original file location.
-        if is_apple_mobile:
+        if needs_apple_fworks:
             assert cls.LOADER is AppleFrameworkLoader
 
             cls.ORIGIN = spec.origin
diff --git a/Lib/test/test_importlib/extension/test_finder.py b/Lib/test/test_importlib/extension/test_finder.py
index cdc8884d668..d1b525f6419 100644
--- a/Lib/test/test_importlib/extension/test_finder.py
+++ b/Lib/test/test_importlib/extension/test_finder.py
@@ -1,4 +1,4 @@
-from test.support import is_apple_mobile
+from test.support import needs_apple_fworks
 from test.test_importlib import abc, util
 
 machinery = util.import_importlib('importlib.machinery')
@@ -20,7 +20,7 @@ def setUp(self):
             )
 
     def find_spec(self, fullname):
-        if is_apple_mobile:
+        if needs_apple_fworks:
             # Apple mobile platforms require a specialist loader that uses
             # .fwork files as placeholders for the true `.so` files.
             loaders = [
diff --git a/Lib/test/test_importlib/extension/test_loader.py b/Lib/test/test_importlib/extension/test_loader.py
index 0dd21e079eb..efae18d4bbd 100644
--- a/Lib/test/test_importlib/extension/test_loader.py
+++ b/Lib/test/test_importlib/extension/test_loader.py
@@ -1,4 +1,4 @@
-from test.support import is_apple_mobile
+from test.support import needs_apple_fworks
 from test.test_importlib import abc, util
 
 machinery = util.import_importlib('importlib.machinery')
@@ -28,7 +28,7 @@ def setUp(self):
 
         # Apple extensions must be distributed as frameworks. This requires
         # a specialist loader.
-        if is_apple_mobile:
+        if needs_apple_fworks:
             self.LoaderClass = self.machinery.AppleFrameworkLoader
         else:
             self.LoaderClass = self.machinery.ExtensionFileLoader
@@ -110,7 +110,7 @@ def setUp(self):
 
         # Apple extensions must be distributed as frameworks. This requires
         # a specialist loader.
-        if is_apple_mobile:
+        if needs_apple_fworks:
             self.LoaderClass = self.machinery.AppleFrameworkLoader
         else:
             self.LoaderClass = self.machinery.ExtensionFileLoader
@@ -198,7 +198,7 @@ def setUp(self):
 
         # Apple extensions must be distributed as frameworks. This requires
         # a specialist loader.
-        if is_apple_mobile:
+        if needs_apple_fworks:
             self.LoaderClass = self.machinery.AppleFrameworkLoader
         else:
             self.LoaderClass = self.machinery.ExtensionFileLoader
diff --git a/Lib/test/test_importlib/test_util.py b/Lib/test/test_importlib/test_util.py
index 5de89714eb5..0c6661791ca 100644
--- a/Lib/test/test_importlib/test_util.py
+++ b/Lib/test/test_importlib/test_util.py
@@ -714,7 +714,7 @@ def test_single_phase_init_module(self):
     def test_incomplete_multi_phase_init_module(self):
         # Apple extensions must be distributed as frameworks. This requires
         # a specialist loader.
-        if support.is_apple_mobile:
+        if support.needs_apple_fworks:
             loader = "AppleFrameworkLoader"
         else:
             loader = "ExtensionFileLoader"
diff --git a/Lib/test/test_importlib/util.py b/Lib/test/test_importlib/util.py
index edbe78545a2..d1f6965196f 100644
--- a/Lib/test/test_importlib/util.py
+++ b/Lib/test/test_importlib/util.py
@@ -8,7 +8,7 @@
 import os.path
 from test import support
 from test.support import import_helper
-from test.support import is_apple_mobile
+from test.support import needs_apple_fworks
 from test.support import os_helper
 import unittest
 import sys
@@ -48,7 +48,7 @@ def _extension_details():
             for ext in machinery.EXTENSION_SUFFIXES:
                 # Apple mobile platforms mechanically load .so files,
                 # but the findable files are labelled .fwork
-                if is_apple_mobile:
+                if needs_apple_fworks:
                     ext = ext.replace(".so", ".fwork")
 
                 filename = EXTENSIONS.name + ext
diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py
index 88b5b0e6e35..c5121fee00e 100644
--- a/Lib/test/test_os.py
+++ b/Lib/test/test_os.py
@@ -2510,19 +2510,54 @@ def test_isatty(self):
 
     @unittest.skipUnless(hasattr(os, 'closerange'), 'test needs os.closerange()')
     def test_closerange(self):
-        fd = os_helper.make_bad_fd()
-        # Make sure none of the descriptors we are about to close are
-        # currently valid (issue 6542).
-        for i in range(10):
-            try: os.fstat(fd+i)
-            except OSError:
-                pass
-            else:
-                break
-        if i < 2:
-            raise unittest.SkipTest(
-                "Unable to acquire a range of invalid file descriptors")
-        self.assertEqual(os.closerange(fd, fd + i-1), None)
+        if support.is_mac_catalyst:
+            # On Mac Catalyst and potentially other Apple platforms,
+            # somehow there's a guarded FD in the way, that has no
+            # effect on fstat. We make some random FDs first, stop
+            # once not consecutive, close all of them one by one by calling
+            # file.close(), then assert that closerange on the FDs is failing.
+            # This ensures that none of those things we're closing is
+            # guarded, if we're careful to not use code that makes guarded
+            # file descriptors between when we manually made and
+            # destroyed them and when we're attempting to invoke
+            # closerange.
+
+            copies = []
+            # Open a file for testing and get its FD
+            file = open(os_helper.TESTFN, "wb")
+            fd = file.fileno()
+            copies.append(file)
+            for i in range(1, 10):
+                file_dup = open(os_helper.TESTFN, "wb")
+                if file_dup.fileno() != fd + i:
+                    file_dup.close()
+                    break
+                else:
+                    copies.append(file_dup)
+            # Close everything
+            for copy in copies:
+                copy.close()
+            os.unlink(os_helper.TESTFN)
+            if i < 2:
+                raise unittest.SkipTest(
+                    "Unable to acquire a range of invalid file descriptors")
+
+            # Now we're left with invalid FDs. Let's go close them!
+            self.assertEqual(os.closerange(fd, fd + i-1), None)
+        else:
+            fd = os_helper.make_bad_fd()
+            # Make sure none of the descriptors we are about to close are
+            # currently valid (issue 6542).
+            for i in range(10):
+                try: os.fstat(fd+i)
+                except OSError:
+                    pass
+                else:
+                    break
+            if i < 2:
+                raise unittest.SkipTest(
+                    "Unable to acquire a range of invalid file descriptors")
+            self.assertEqual(os.closerange(fd, fd + i-1), None)
 
     @unittest.skipUnless(hasattr(os, 'dup2'), 'test needs os.dup2()')
     def test_dup2(self):
diff --git a/Lib/test/test_platform.py b/Lib/test/test_platform.py
index 92a831a9148..2f4f221fe4f 100644
--- a/Lib/test/test_platform.py
+++ b/Lib/test/test_platform.py
@@ -488,7 +488,7 @@ def test_ios_ver(self):
 
         # ios_ver is only fully available on iOS where ctypes is available.
         if sys.platform == "ios" and _ctypes:
-            system, release, model, is_simulator = result
+            system, release, model, is_simulator, is_catalyst = result
             # Result is a namedtuple
             self.assertEqual(result.system, system)
             self.assertEqual(result.release, release)
@@ -499,6 +499,7 @@ def test_ios_ver(self):
             # ios_ver(), so we check that the values are broadly what we expect.
 
             # System is either iOS or iPadOS, depending on the test device
+            # Mac Catalyst returns iPadOS.
             self.assertIn(system, {"iOS", "iPadOS"})
 
             # Release is a numeric version specifier with at least 2 parts
@@ -511,6 +512,9 @@ def test_ios_ver(self):
             # we get a model descriptor like "iPhone13,1"
             if is_simulator:
                 self.assertIn(model, {"iPhone", "iPad"})
+            # Mac Catalyst identifies as iPad with no version.
+            elif is_catalyst:
+                self.assertEqual(model, "iPad")
             else:
                 self.assertTrue(
                     (model.startswith("iPhone") or model.startswith("iPad"))
@@ -518,6 +522,10 @@ def test_ios_ver(self):
                 )
 
             self.assertEqual(type(is_simulator), bool)
+
+            # Mac Catalyst platform will return iPadOS.
+            if is_catalyst:
+                self.assertEqual(system, "iPadOS")
         else:
             # On non-iOS platforms, calling ios_ver doesn't fail; you get
             # default values
@@ -527,11 +535,12 @@ def test_ios_ver(self):
             self.assertFalse(result.is_simulator)
 
             # Check the fallback values can be overridden by arguments
-            override = platform.ios_ver("Foo", "Bar", "Whiz", True)
+            override = platform.ios_ver("Foo", "Bar", "Whiz", True, True)
             self.assertEqual(override.system, "Foo")
             self.assertEqual(override.release, "Bar")
             self.assertEqual(override.model, "Whiz")
             self.assertTrue(override.is_simulator)
+            self.assertTrue(override.is_catalyst)
 
     @unittest.skipIf(support.is_emscripten, "Does not apply to Emscripten")
     def test_libc_ver(self):
diff --git a/MacCatalyst/Resources/Info.plist.in b/MacCatalyst/Resources/Info.plist.in
new file mode 100644
index 00000000000..80b5664508e
--- /dev/null
+++ b/MacCatalyst/Resources/Info.plist.in
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist SYSTEM "file://localhost/System/Library/DTDs/PropertyList.dtd">
+<plist version="0.9">
+<dict>
+	<key>CFBundleDevelopmentRegion</key>
+	<string>en</string>
+	<key>CFBundleExecutable</key>
+	<string>Python</string>
+	<key>CFBundleGetInfoString</key>
+	<string>Python Runtime and Library</string>
+	<key>CFBundleIdentifier</key>
+	<string>@PYTHONFRAMEWORKIDENTIFIER@</string>
+	<key>CFBundleInfoDictionaryVersion</key>
+	<string>6.0</string>
+	<key>CFBundleName</key>
+	<string>Python</string>
+	<key>CFBundlePackageType</key>
+	<string>FMWK</string>
+	<key>CFBundleShortVersionString</key>
+	<string>%VERSION%</string>
+	<key>CFBundleLongVersionString</key>
+	<string>%VERSION%, (c) 2001-2024 Python Software Foundation.</string>
+	<key>CFBundleSignature</key>
+	<string>????</string>
+	<key>CFBundleVersion</key>
+	<string>%VERSION%</string>
+	<key>CFBundleSupportedPlatforms</key>
+	<array>
+		<string>MacOSX</string>
+	</array>
+	<key>LSMinimumSystemVersion</key>
+	<string>@CATALYST_MACOS_VERSION@</string>
+	<key>UIDeviceFamily</key>
+	<array>
+		<integer>2</integer>
+	</array>
+</dict>
+</plist>
diff --git a/MacCatalyst/Resources/bin/arm64-apple-ios-macabi-ar b/MacCatalyst/Resources/bin/arm64-apple-ios-macabi-ar
new file mode 100755
index 00000000000..1e31b940c23
--- /dev/null
+++ b/MacCatalyst/Resources/bin/arm64-apple-ios-macabi-ar
@@ -0,0 +1,2 @@
+#!/bin/sh
+xcrun --sdk macosx${MACOSX_SDK_VERSION} ar "$@"
diff --git a/MacCatalyst/Resources/bin/arm64-apple-ios-macabi-clang b/MacCatalyst/Resources/bin/arm64-apple-ios-macabi-clang
new file mode 100755
index 00000000000..6845b5d1b20
--- /dev/null
+++ b/MacCatalyst/Resources/bin/arm64-apple-ios-macabi-clang
@@ -0,0 +1,2 @@
+#!/bin/sh
+xcrun --sdk macosx${MACOSX_SDK_VERSION} clang -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-macabi "$@"
diff --git a/MacCatalyst/Resources/bin/arm64-apple-ios-macabi-clang++ b/MacCatalyst/Resources/bin/arm64-apple-ios-macabi-clang++
new file mode 100755
index 00000000000..779c3166268
--- /dev/null
+++ b/MacCatalyst/Resources/bin/arm64-apple-ios-macabi-clang++
@@ -0,0 +1,2 @@
+#!/bin/sh
+xcrun --sdk macosx${MACOSX_SDK_VERSION} clang++ -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-macabi "$@"
diff --git a/MacCatalyst/Resources/bin/arm64-apple-ios-macabi-cpp b/MacCatalyst/Resources/bin/arm64-apple-ios-macabi-cpp
new file mode 100755
index 00000000000..01ff56a0f0e
--- /dev/null
+++ b/MacCatalyst/Resources/bin/arm64-apple-ios-macabi-cpp
@@ -0,0 +1,2 @@
+#!/bin/sh
+xcrun --sdk macosx${MACOSX_SDK_VERSION} clang -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-macabi -E "$@"
diff --git a/MacCatalyst/Resources/bin/x86_64-apple-ios-macabi-ar b/MacCatalyst/Resources/bin/x86_64-apple-ios-macabi-ar
new file mode 100755
index 00000000000..1e31b940c23
--- /dev/null
+++ b/MacCatalyst/Resources/bin/x86_64-apple-ios-macabi-ar
@@ -0,0 +1,2 @@
+#!/bin/sh
+xcrun --sdk macosx${MACOSX_SDK_VERSION} ar "$@"
diff --git a/MacCatalyst/Resources/bin/x86_64-apple-ios-macabi-clang b/MacCatalyst/Resources/bin/x86_64-apple-ios-macabi-clang
new file mode 100755
index 00000000000..a3170bb1f15
--- /dev/null
+++ b/MacCatalyst/Resources/bin/x86_64-apple-ios-macabi-clang
@@ -0,0 +1,2 @@
+#!/bin/sh
+xcrun --sdk macosx${MACOSX_SDK_VERSION} clang -target x86_64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-macabi "$@"
diff --git a/MacCatalyst/Resources/bin/x86_64-apple-ios-macabi-clang++ b/MacCatalyst/Resources/bin/x86_64-apple-ios-macabi-clang++
new file mode 100755
index 00000000000..d3161b335e1
--- /dev/null
+++ b/MacCatalyst/Resources/bin/x86_64-apple-ios-macabi-clang++
@@ -0,0 +1,2 @@
+#!/bin/sh
+xcrun --sdk macosx${MACOSX_SDK_VERSION} clang++ -target x86_64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-macabi "$@"
diff --git a/MacCatalyst/Resources/bin/x86_64-apple-ios-macabi-cpp b/MacCatalyst/Resources/bin/x86_64-apple-ios-macabi-cpp
new file mode 100755
index 00000000000..7e3e9124123
--- /dev/null
+++ b/MacCatalyst/Resources/bin/x86_64-apple-ios-macabi-cpp
@@ -0,0 +1,2 @@
+#!/bin/sh
+xcrun --sdk macosx${MACOSX_SDK_VERSION} clang -target x86_64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-macabi -E "$@"
diff --git a/MacCatalyst/Resources/pyconfig.h b/MacCatalyst/Resources/pyconfig.h
new file mode 100644
index 00000000000..4acff2c6051
--- /dev/null
+++ b/MacCatalyst/Resources/pyconfig.h
@@ -0,0 +1,7 @@
+#ifdef __arm64__
+#include "pyconfig-arm64.h"
+#endif
+
+#ifdef __x86_64__
+#include "pyconfig-x86_64.h"
+#endif
diff --git a/Makefile.pre.in b/Makefile.pre.in
index e436b2efb8e..d8df7de3102 100644
--- a/Makefile.pre.in
+++ b/Makefile.pre.in
@@ -2277,10 +2277,6 @@ testios:
 		echo "Cannot run the iOS testbed for a non-iOS build."; \
 		exit 1;\
 	fi
-	@if test "$(findstring -iphonesimulator,$(MULTIARCH))" != "-iphonesimulator"; then \
-		echo "Cannot run the iOS testbed for non-simulator builds."; \
-		exit 1;\
-	fi
 	@if test $(PYTHONFRAMEWORK) != "Python"; then \
 		echo "Cannot run the iOS testbed with a non-default framework name."; \
 		exit 1;\
@@ -2290,11 +2286,18 @@ testios:
 		exit 1;\
 	fi
 
-	# Clone the testbed project into the XCFOLDER-iOS
-	$(PYTHON_FOR_BUILD) $(srcdir)/iOS/testbed clone --framework $(PYTHONFRAMEWORKPREFIX) "$(XCFOLDER-iOS)"
-
-	# Run the testbed project
-	$(PYTHON_FOR_BUILD) "$(XCFOLDER-iOS)" run --verbose -- test -uall --single-process --rerun -W
+	@if test "$(findstring -iphonesimulator,$(MULTIARCH))" != "-iphonesimulator"; then \
+		if test "$(findstring -macabi,$(MULTIARCH))" != "-macabi"; then \
+			echo "Cannot run the iOS testbed for device builds."; \
+			exit 1;\
+		else \
+			$(PYTHON_FOR_BUILD) $(srcdir)/iOS/testbed --catalyst clone --framework $(PYTHONFRAMEWORKPREFIX) "$(XCFOLDER-iOS)"; \
+			$(PYTHON_FOR_BUILD) "$(XCFOLDER-iOS)" --catalyst run --verbose -- test -uall --single-process --rerun -W; \
+		fi \
+	else \
+		$(PYTHON_FOR_BUILD) $(srcdir)/iOS/testbed clone --framework $(PYTHONFRAMEWORKPREFIX) "$(XCFOLDER-iOS)"; \
+		$(PYTHON_FOR_BUILD) "$(XCFOLDER-iOS)" run --verbose -- test -uall --single-process --rerun -W; \
+	fi
 
 # Run the test suite on the visionOS simulator. Must be run on a macOS machine with
 # a full Xcode install that has an Apple Vision Pro simulator available.
@@ -3013,7 +3016,7 @@ frameworkinstallversionedstructure:	$(LDLIBRARY)
 		fi; \
 	done
 	$(LN) -fsn include/python$(LDVERSION) $(DESTDIR)$(prefix)/Headers
-	sed 's/%VERSION%/'"`$(RUNSHARED) ./$(BUILDPYTHON) -c 'import platform; print(platform.python_version())'`"'/g' < $(RESSRCDIR)/Info.plist > $(DESTDIR)$(prefix)/Resources/Info.plist
+	sed 's/%VERSION%/'"`$(RUNSHARED) ./$(PYTHON_FOR_BUILD) -c 'import platform; print(platform.python_version())'`"'/g' < $(RESSRCDIR)/Info.plist > $(DESTDIR)$(prefix)/Resources/Info.plist
 	$(LN) -fsn $(VERSION) $(DESTDIR)$(PYTHONFRAMEWORKINSTALLDIR)/Versions/Current
 	$(LN) -fsn Versions/Current/$(PYTHONFRAMEWORK) $(DESTDIR)$(PYTHONFRAMEWORKINSTALLDIR)/$(PYTHONFRAMEWORK)
 	$(LN) -fsn Versions/Current/Headers $(DESTDIR)$(PYTHONFRAMEWORKINSTALLDIR)/Headers
@@ -3041,6 +3044,14 @@ frameworkinstallunversionedstructure:	$(LDLIBRARY)
 		$(INSTALL) -m $(EXEMODE) $$file $(DESTDIR)$(BINDIR); \
 	done
 
+# Stub compilation assistance binaries are installed separately on Mac Catalyst.
+.PHONY: frameworkinstallcatalyststubs
+frameworkinstallcatalyststubs:	$(LDLIBRARY)
+	$(INSTALL) -d -m $(DIRMODE) $(DESTDIR)$(BINDIR)
+	for file in $(srcdir)/$(RESSRCDIR)/bin/* ; do \
+		$(INSTALL) -m $(EXEMODE) $$file $(DESTDIR)$(BINDIR); \
+	done
+
 # This installs Mac/Lib into the framework
 # Install a number of symlinks to keep software that expects a normal unix
 # install (which includes python-config) happy.
diff --git a/Misc/platform_triplet.c b/Misc/platform_triplet.c
index 6c1863c943b..ba634ae1699 100644
--- a/Misc/platform_triplet.c
+++ b/Misc/platform_triplet.c
@@ -254,6 +254,12 @@ PLATFORM_TRIPLET=x86_64-iphonesimulator
 #      else
 PLATFORM_TRIPLET=arm64-iphonesimulator
 #      endif
+#    elif defined(TARGET_OS_MACCATALYST) && TARGET_OS_MACCATALYST
+#      if __x86_64__
+PLATFORM_TRIPLET=x86_64-iphoneos-macabi
+#      else
+PLATFORM_TRIPLET=arm64-iphoneos-macabi
+#      endif
 #    else
 PLATFORM_TRIPLET=arm64-iphoneos
 #    endif
diff --git a/config.sub b/config.sub
index 49febd56a37..6efa3fbf821 100755
--- a/config.sub
+++ b/config.sub
@@ -1769,7 +1769,7 @@ case $os in
 	     | onefs* | tirtos* | phoenix* | fuchsia* | redox* | bme* \
 	     | midnightbsd* | amdhsa* | unleashed* | emscripten* | wasi* \
 	     | nsk* | powerunix* | genode* | zvmoe* | qnx* | emx* | zephyr* \
-	     | fiwix* | mlibc* | cos* | mbr* | ironclad* )
+	     | fiwix* | mlibc* | cos* | mbr* | ironclad* | macabi)
 		;;
 	# This one is extra strict with allowed versions
 	sco3.2v2 | sco3.2v[4-9]* | sco5v6*)
@@ -1869,6 +1869,8 @@ case $kernel-$os-$obj in
 		;;
 	ios*-simulator- | tvos*-simulator- | watchos*-simulator- | xros*-simulator-)
 		;;
+	ios*-macabi- )
+		;;
 	none--*)
 		# None (no kernel, i.e. freestanding / bare metal),
 		# can be paired with an machine code file format
diff --git a/configure b/configure
index 7c93d36e717..5710b745618 100755
--- a/configure
+++ b/configure
@@ -982,6 +982,7 @@ LDFLAGS
 CFLAGS
 CC
 HAS_XCRUN
+CATALYST_MACOS_VERSION
 EXPORT_XROS_DEPLOYMENT_TARGET
 XROS_DEPLOYMENT_TARGET
 WATCHOS_DEPLOYMENT_TARGET
@@ -1089,6 +1090,7 @@ with_universal_archs
 with_framework_name
 enable_framework
 with_app_store_compliance
+with_catalyst_macos_version
 enable_wasm_dynamic_linking
 enable_wasm_pthreads
 with_suffix
@@ -1878,6 +1880,9 @@ Optional Packages:
                           Enable any patches required for compiliance with app
                           stores. Optional PATCH-FILE specifies the custom
                           patch to apply.
+  --with-catalyst-macos-version=VER
+                          The minimum macOS version a Catalyst build can run
+                          on (only valid for *-apple-ios*-macabi targets)
   --with-suffix=SUFFIX    set executable suffix to SUFFIX (default is empty,
                           yes is mapped to '.exe')
   --without-static-libpython
@@ -4222,9 +4227,12 @@ fi
 # configure will fail.
 if test -z "$AR"; then
 	case "$host" in
+		x86_64-apple-ios*-macabi)  AR=x86_64-apple-ios-macabi-ar ;;
+		aarch64-apple-ios*-macabi) AR=arm64-apple-ios-macabi-ar ;;
+
+		x86_64-apple-ios*-simulator)  AR=x86_64-apple-ios-simulator-ar ;;
 		aarch64-apple-ios*-simulator) AR=arm64-apple-ios-simulator-ar ;;
 		aarch64-apple-ios*)           AR=arm64-apple-ios-ar ;;
-		x86_64-apple-ios*-simulator)  AR=x86_64-apple-ios-simulator-ar ;;
 
 		aarch64-apple-tvos*-simulator) AR=arm64-apple-tvos-simulator-ar ;;
 		aarch64-apple-tvos*)           AR=arm64-apple-tvos-ar ;;
@@ -4241,9 +4249,12 @@ if test -z "$AR"; then
 fi
 if test -z "$CC"; then
 	case "$host" in
+		x86_64-apple-ios*-macabi)  CC=x86_64-apple-ios-macabi-clang ;;
+		aarch64-apple-ios*-macabi) CC=arm64-apple-ios-macabi-clang ;;
+
+		x86_64-apple-ios*-simulator)  CC=x86_64-apple-ios-simulator-clang ;;
 		aarch64-apple-ios*-simulator) CC=arm64-apple-ios-simulator-clang ;;
 		aarch64-apple-ios*)           CC=arm64-apple-ios-clang ;;
-		x86_64-apple-ios*-simulator)  CC=x86_64-apple-ios-simulator-clang ;;
 
 		aarch64-apple-tvos*-simulator) CC=arm64-apple-tvos-simulator-clang ;;
 		aarch64-apple-tvos*)           CC=arm64-apple-tvos-clang ;;
@@ -4260,9 +4271,12 @@ if test -z "$CC"; then
 fi
 if test -z "$CPP"; then
 	case "$host" in
+		x86_64-apple-ios*-macabi)  CPP=x86_64-apple-ios-macabi-cpp ;;
+		aarch64-apple-ios*-macabi) CPP=arm64-apple-ios-macabi-cpp ;;
+
+		x86_64-apple-ios*-simulator)  CPP=x86_64-apple-ios-simulator-cpp ;;
 		aarch64-apple-ios*-simulator) CPP=arm64-apple-ios-simulator-cpp ;;
 		aarch64-apple-ios*)           CPP=arm64-apple-ios-cpp ;;
-		x86_64-apple-ios*-simulator)  CPP=x86_64-apple-ios-simulator-cpp ;;
 
 		aarch64-apple-tvos*-simulator) CPP=arm64-apple-tvos-simulator-cpp ;;
 		aarch64-apple-tvos*)           CPP=arm64-apple-tvos-cpp ;;
@@ -4279,9 +4293,12 @@ if test -z "$CPP"; then
 fi
 if test -z "$CXX"; then
 	case "$host" in
+		x86_64-apple-ios*-macabi)  CXX=x86_64-apple-ios-macabi-clang++ ;;
+		aarch64-apple-ios*-macabi) CXX=arm64-apple-ios-macabi-clang++ ;;
+
+		x86_64-apple-ios*-simulator)  CXX=x86_64-apple-ios-simulator-clang++ ;;
 		aarch64-apple-ios*-simulator) CXX=arm64-apple-ios-simulator-clang++ ;;
 		aarch64-apple-ios*)           CXX=arm64-apple-ios-clang++ ;;
-		x86_64-apple-ios*-simulator)  CXX=x86_64-apple-ios-simulator-clang++ ;;
 
 		aarch64-apple-tvos*-simulator) CXX=arm64-apple-tvos-simulator-clang++ ;;
 		aarch64-apple-tvos*)           CXX=arm64-apple-tvos-clang++ ;;
@@ -4524,19 +4541,34 @@ then :
 
 				;;
 			iOS) :
-				FRAMEWORKINSTALLFIRST="frameworkinstallunversionedstructure"
-				FRAMEWORKALTINSTALLFIRST="frameworkinstallunversionedstructure "
-				FRAMEWORKINSTALLLAST="frameworkinstallmobileheaders"
-				FRAMEWORKALTINSTALLLAST="frameworkinstallmobileheaders"
-				FRAMEWORKPYTHONW=
-				INSTALLTARGETS="libinstall inclinstall sharedinstall"
-
-				prefix=$PYTHONFRAMEWORKPREFIX
-				PYTHONFRAMEWORKINSTALLNAMEPREFIX="@rpath/$PYTHONFRAMEWORKDIR"
-				RESSRCDIR=iOS/Resources
-
-				ac_config_files="$ac_config_files iOS/Resources/Info.plist"
+				case $(echo $host | cut -d '-' -f4) in
+					macabi)
+						FRAMEWORKINSTALLFIRST="frameworkinstallversionedstructure frameworkinstallcatalyststubs"
+						FRAMEWORKALTINSTALLFIRST="frameworkinstallversionedstructure frameworkinstallcatalyststubs "
+						FRAMEWORKINSTALLLAST=""
+						FRAMEWORKALTINSTALLLAST=""
+						FRAMEWORKPYTHONW=
+						INSTALLTARGETS="libinstall inclinstall sharedinstall"
+						prefix=$PYTHONFRAMEWORKINSTALLDIR/Versions/$VERSION
+						PYTHONFRAMEWORKINSTALLNAMEPREFIX="@rpath/$PYTHONFRAMEWORKDIR/Versions/$VERSION"
+						RESSRCDIR=MacCatalyst/Resources
+						ac_config_files="$ac_config_files MacCatalyst/Resources/Info.plist"
+
+						;;
+					*)
+						FRAMEWORKINSTALLFIRST="frameworkinstallunversionedstructure"
+						FRAMEWORKALTINSTALLFIRST="frameworkinstallunversionedstructure "
+						FRAMEWORKINSTALLLAST="frameworkinstallmobileheaders"
+						FRAMEWORKALTINSTALLLAST="frameworkinstallmobileheaders"
+						FRAMEWORKPYTHONW=
+						INSTALLTARGETS="libinstall inclinstall sharedinstall"
+						prefix=$PYTHONFRAMEWORKPREFIX
+						PYTHONFRAMEWORKINSTALLNAMEPREFIX="@rpath/$PYTHONFRAMEWORKDIR"
+						RESSRCDIR=iOS/Resources
+						ac_config_files="$ac_config_files iOS/Resources/Info.plist"
 
+					;;
+				esac
 				;;
 			tvOS) :
 				FRAMEWORKINSTALLFIRST="frameworkinstallunversionedstructure"
@@ -4688,6 +4720,34 @@ fi
 
 
 
+
+
+# Check whether --with-catalyst-macos-version was given.
+if test ${with_catalyst_macos_version+y}
+then :
+  withval=$with_catalyst_macos_version; case "$host" in
+    *-apple-ios*-macabi)
+      CATALYST_MACOS_VERSION="$withval"
+      ;;
+    *)
+      as_fn_error $? "--with-catalyst-macos-version is only valid when targeting Mac Catalyst (*-apple-ios*-macabi)." "$LINENO" 5
+      ;;
+   esac
+
+else case e in #(
+  e) case "$host" in
+    *-apple-ios*-macabi)
+      CATALYST_MACOS_VERSION=11.2
+      ;;
+    *)
+      CATALYST_MACOS_VERSION=
+      ;;
+    esac
+
+ ;;
+esac
+fi
+
 EXPORT_XROS_DEPLOYMENT_TARGET='#'
 
 
@@ -4705,6 +4765,28 @@ if test "$cross_compiling" = yes; then
 	*-*-cygwin*)
 		_host_ident=
 		;;
+	*-apple-ios*-macabi)
+		_host_os=`echo $host | cut -d '-' -f3`
+		_host_device=`echo $host | cut -d '-' -f4`   # should be macabi
+		_host_device=${_host_device:=os}
+
+		# IPHONEOS_DEPLOYMENT_TARGET is the minimum supported iOS version
+		{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking iOS deployment target" >&5
+printf %s "checking iOS deployment target... " >&6; }
+		IPHONEOS_DEPLOYMENT_TARGET=$(echo ${_host_os} | cut -c4-)
+		IPHONEOS_DEPLOYMENT_TARGET=${IPHONEOS_DEPLOYMENT_TARGET:=14.2}  # else it returns invalid version number
+		{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $IPHONEOS_DEPLOYMENT_TARGET" >&5
+printf "%s\n" "$IPHONEOS_DEPLOYMENT_TARGET" >&6; }
+
+		case "$host_cpu" in
+			aarch64)
+				_host_ident=${IPHONEOS_DEPLOYMENT_TARGET}-arm64-iphoneos-${_host_device}   # platform_triplet.c uses iphoneos-macabi
+				;;
+			*)
+				_host_ident=${IPHONEOS_DEPLOYMENT_TARGET}-$host_cpu-iphoneos-${_host_device}
+				;;
+		esac
+		;;
 	*-apple-ios*)
 		_host_os=`echo $host | cut -d '-' -f3`
 		_host_device=`echo $host | cut -d '-' -f4`
@@ -4969,6 +5051,10 @@ EXPORT_MACOSX_DEPLOYMENT_TARGET='#'
 # XROS_DEPLOYMENT_TARGET should get exported
 
 
+# The minimum macOS version that a Mac Catalyst build can run on.
+
+
+
 # checks for alternative programs
 
 # compiler flags are generated in two sets, BASECFLAGS and OPT.  OPT is just
@@ -7459,6 +7545,10 @@ case $host/$ac_cv_cc_name in #(
     PY_SUPPORT_TIER=3 ;; #(
     x86_64-*-freebsd*/clang) :
     PY_SUPPORT_TIER=3 ;; #(
+    aarch64-apple-ios*-macabi/clang) :
+    PY_SUPPORT_TIER=3 ;; #(
+    x86_64-apple-ios*-macabi/clang) :
+    PY_SUPPORT_TIER=3 ;; #(
     aarch64-apple-ios*-simulator/clang) :
     PY_SUPPORT_TIER=3 ;; #(
     aarch64-apple-ios*/clang) :
@@ -31344,6 +31434,13 @@ case $ac_sys_system in #(
      ;; #(
   iOS|tvOS|watchOS|visionOS) :
 
+                        case "$_host_device" in
+      macabi) ;;
+      *)
+
+    py_cv_module__posixsubprocess=n/a
+ ;;
+    esac
 
 
     py_cv_module__curses=n/a
@@ -31351,7 +31448,6 @@ case $ac_sys_system in #(
     py_cv_module__gdbm=n/a
     py_cv_module__multiprocessing=n/a
     py_cv_module__posixshmem=n/a
-    py_cv_module__posixsubprocess=n/a
     py_cv_module__scproxy=n/a
     py_cv_module__tkinter=n/a
     py_cv_module_grp=n/a
@@ -35503,6 +35599,7 @@ do
     "Mac/PythonLauncher/Makefile") CONFIG_FILES="$CONFIG_FILES Mac/PythonLauncher/Makefile" ;;
     "Mac/Resources/framework/Info.plist") CONFIG_FILES="$CONFIG_FILES Mac/Resources/framework/Info.plist" ;;
     "Mac/Resources/app/Info.plist") CONFIG_FILES="$CONFIG_FILES Mac/Resources/app/Info.plist" ;;
+    "MacCatalyst/Resources/Info.plist") CONFIG_FILES="$CONFIG_FILES MacCatalyst/Resources/Info.plist" ;;
     "iOS/Resources/Info.plist") CONFIG_FILES="$CONFIG_FILES iOS/Resources/Info.plist" ;;
     "tvOS/Resources/Info.plist") CONFIG_FILES="$CONFIG_FILES tvOS/Resources/Info.plist" ;;
     "watchOS/Resources/Info.plist") CONFIG_FILES="$CONFIG_FILES watchOS/Resources/Info.plist" ;;
diff --git a/configure.ac b/configure.ac
index 7ab0609bf8a..1c47cbef67d 100644
--- a/configure.ac
+++ b/configure.ac
@@ -426,9 +426,12 @@ AC_SUBST([host_exec_prefix])
 # configure will fail.
 if test -z "$AR"; then
 	case "$host" in
+		x86_64-apple-ios*-macabi)  AR=x86_64-apple-ios-macabi-ar ;;
+		aarch64-apple-ios*-macabi) AR=arm64-apple-ios-macabi-ar ;;
+
+		x86_64-apple-ios*-simulator)  AR=x86_64-apple-ios-simulator-ar ;;
 		aarch64-apple-ios*-simulator) AR=arm64-apple-ios-simulator-ar ;;
 		aarch64-apple-ios*)           AR=arm64-apple-ios-ar ;;
-		x86_64-apple-ios*-simulator)  AR=x86_64-apple-ios-simulator-ar ;;
 
 		aarch64-apple-tvos*-simulator) AR=arm64-apple-tvos-simulator-ar ;;
 		aarch64-apple-tvos*)           AR=arm64-apple-tvos-ar ;;
@@ -445,9 +448,12 @@ if test -z "$AR"; then
 fi
 if test -z "$CC"; then
 	case "$host" in
+		x86_64-apple-ios*-macabi)  CC=x86_64-apple-ios-macabi-clang ;;
+		aarch64-apple-ios*-macabi) CC=arm64-apple-ios-macabi-clang ;;
+
+		x86_64-apple-ios*-simulator)  CC=x86_64-apple-ios-simulator-clang ;;
 		aarch64-apple-ios*-simulator) CC=arm64-apple-ios-simulator-clang ;;
 		aarch64-apple-ios*)           CC=arm64-apple-ios-clang ;;
-		x86_64-apple-ios*-simulator)  CC=x86_64-apple-ios-simulator-clang ;;
 
 		aarch64-apple-tvos*-simulator) CC=arm64-apple-tvos-simulator-clang ;;
 		aarch64-apple-tvos*)           CC=arm64-apple-tvos-clang ;;
@@ -464,9 +470,12 @@ if test -z "$CC"; then
 fi
 if test -z "$CPP"; then
 	case "$host" in
+		x86_64-apple-ios*-macabi)  CPP=x86_64-apple-ios-macabi-cpp ;;
+		aarch64-apple-ios*-macabi) CPP=arm64-apple-ios-macabi-cpp ;;
+
+		x86_64-apple-ios*-simulator)  CPP=x86_64-apple-ios-simulator-cpp ;;
 		aarch64-apple-ios*-simulator) CPP=arm64-apple-ios-simulator-cpp ;;
 		aarch64-apple-ios*)           CPP=arm64-apple-ios-cpp ;;
-		x86_64-apple-ios*-simulator)  CPP=x86_64-apple-ios-simulator-cpp ;;
 
 		aarch64-apple-tvos*-simulator) CPP=arm64-apple-tvos-simulator-cpp ;;
 		aarch64-apple-tvos*)           CPP=arm64-apple-tvos-cpp ;;
@@ -483,9 +492,12 @@ if test -z "$CPP"; then
 fi
 if test -z "$CXX"; then
 	case "$host" in
+		x86_64-apple-ios*-macabi)  CXX=x86_64-apple-ios-macabi-clang++ ;;
+		aarch64-apple-ios*-macabi) CXX=arm64-apple-ios-macabi-clang++ ;;
+
+		x86_64-apple-ios*-simulator)  CXX=x86_64-apple-ios-simulator-clang++ ;;
 		aarch64-apple-ios*-simulator) CXX=arm64-apple-ios-simulator-clang++ ;;
 		aarch64-apple-ios*)           CXX=arm64-apple-ios-clang++ ;;
-		x86_64-apple-ios*-simulator)  CXX=x86_64-apple-ios-simulator-clang++ ;;
 
 		aarch64-apple-tvos*-simulator) CXX=arm64-apple-tvos-simulator-clang++ ;;
 		aarch64-apple-tvos*)           CXX=arm64-apple-tvos-clang++ ;;
@@ -716,18 +728,32 @@ AC_ARG_ENABLE([framework],
 				AC_CONFIG_FILES([Mac/Resources/app/Info.plist])
 				;;
 			iOS) :
-				FRAMEWORKINSTALLFIRST="frameworkinstallunversionedstructure"
-				FRAMEWORKALTINSTALLFIRST="frameworkinstallunversionedstructure "
-				FRAMEWORKINSTALLLAST="frameworkinstallmobileheaders"
-				FRAMEWORKALTINSTALLLAST="frameworkinstallmobileheaders"
-				FRAMEWORKPYTHONW=
-				INSTALLTARGETS="libinstall inclinstall sharedinstall"
-
-				prefix=$PYTHONFRAMEWORKPREFIX
-				PYTHONFRAMEWORKINSTALLNAMEPREFIX="@rpath/$PYTHONFRAMEWORKDIR"
-				RESSRCDIR=iOS/Resources
-
-				AC_CONFIG_FILES([iOS/Resources/Info.plist])
+				case $(echo $host | cut -d '-' -f4) in
+					macabi)
+						FRAMEWORKINSTALLFIRST="frameworkinstallversionedstructure frameworkinstallcatalyststubs"
+						FRAMEWORKALTINSTALLFIRST="frameworkinstallversionedstructure frameworkinstallcatalyststubs "
+						FRAMEWORKINSTALLLAST=""
+						FRAMEWORKALTINSTALLLAST=""
+						FRAMEWORKPYTHONW=
+						INSTALLTARGETS="libinstall inclinstall sharedinstall"
+						prefix=$PYTHONFRAMEWORKINSTALLDIR/Versions/$VERSION
+						PYTHONFRAMEWORKINSTALLNAMEPREFIX="@rpath/$PYTHONFRAMEWORKDIR/Versions/$VERSION"
+						RESSRCDIR=MacCatalyst/Resources
+						AC_CONFIG_FILES([MacCatalyst/Resources/Info.plist])
+						;;
+					*)
+						FRAMEWORKINSTALLFIRST="frameworkinstallunversionedstructure"
+						FRAMEWORKALTINSTALLFIRST="frameworkinstallunversionedstructure "
+						FRAMEWORKINSTALLLAST="frameworkinstallmobileheaders"
+						FRAMEWORKALTINSTALLLAST="frameworkinstallmobileheaders"
+						FRAMEWORKPYTHONW=
+						INSTALLTARGETS="libinstall inclinstall sharedinstall"
+						prefix=$PYTHONFRAMEWORKPREFIX
+						PYTHONFRAMEWORKINSTALLNAMEPREFIX="@rpath/$PYTHONFRAMEWORKDIR"
+						RESSRCDIR=iOS/Resources
+						AC_CONFIG_FILES([iOS/Resources/Info.plist])
+					;;
+				esac
 				;;
 			tvOS) :
 				FRAMEWORKINSTALLFIRST="frameworkinstallunversionedstructure"
@@ -863,6 +889,29 @@ AC_ARG_WITH(
 ])
 AC_SUBST([APP_STORE_COMPLIANCE_PATCH])
 
+
+AC_ARG_WITH([catalyst-macos-version],
+  [AS_HELP_STRING([--with-catalyst-macos-version=VER],
+  [The minimum macOS version a Catalyst build can run on (only valid for *-apple-ios*-macabi targets)])],
+  [case "$host" in
+    *-apple-ios*-macabi)
+      CATALYST_MACOS_VERSION="$withval"
+      ;;
+    *)
+      AC_MSG_ERROR([--with-catalyst-macos-version is only valid when targeting Mac Catalyst (*-apple-ios*-macabi).])
+      ;;
+   esac
+  ],
+  [case "$host" in
+    *-apple-ios*-macabi)
+      CATALYST_MACOS_VERSION=11.2
+      ;;
+    *)
+      CATALYST_MACOS_VERSION=
+      ;;
+    esac
+  ]
+)
 EXPORT_XROS_DEPLOYMENT_TARGET='#'
 
 AC_SUBST([_PYTHON_HOST_PLATFORM])
@@ -880,6 +929,26 @@ if test "$cross_compiling" = yes; then
 	*-*-cygwin*)
 		_host_ident=
 		;;
+	*-apple-ios*-macabi)
+		_host_os=`echo $host | cut -d '-' -f3`
+		_host_device=`echo $host | cut -d '-' -f4`   # should be macabi
+		_host_device=${_host_device:=os}
+
+		# IPHONEOS_DEPLOYMENT_TARGET is the minimum supported iOS version
+		AC_MSG_CHECKING([iOS deployment target])
+		IPHONEOS_DEPLOYMENT_TARGET=$(echo ${_host_os} | cut -c4-)
+		IPHONEOS_DEPLOYMENT_TARGET=${IPHONEOS_DEPLOYMENT_TARGET:=14.2}  # else it returns invalid version number
+		AC_MSG_RESULT([$IPHONEOS_DEPLOYMENT_TARGET])
+
+		case "$host_cpu" in
+			aarch64)
+				_host_ident=${IPHONEOS_DEPLOYMENT_TARGET}-arm64-iphoneos-${_host_device}   # platform_triplet.c uses iphoneos-macabi
+				;;
+			*)
+				_host_ident=${IPHONEOS_DEPLOYMENT_TARGET}-$host_cpu-iphoneos-${_host_device}
+				;;
+		esac
+		;;
 	*-apple-ios*)
 		_host_os=`echo $host | cut -d '-' -f3`
 		_host_device=`echo $host | cut -d '-' -f4`
@@ -1129,6 +1198,10 @@ AC_SUBST([XROS_DEPLOYMENT_TARGET])
 # XROS_DEPLOYMENT_TARGET should get exported
 AC_SUBST([EXPORT_XROS_DEPLOYMENT_TARGET])
 
+# The minimum macOS version that a Mac Catalyst build can run on.
+AC_SUBST([CATALYST_MACOS_VERSION])
+
+
 # checks for alternative programs
 
 # compiler flags are generated in two sets, BASECFLAGS and OPT.  OPT is just
@@ -1418,6 +1491,8 @@ AS_CASE([$host/$ac_cv_cc_name],
   [powerpc64le-*-linux-gnu/clang],   [PY_SUPPORT_TIER=3], dnl Linux on PPC64 little endian, glibc, clang
   [s390x-*-linux-gnu/gcc],           [PY_SUPPORT_TIER=3], dnl Linux on 64bit s390x (big endian), glibc, gcc
   [x86_64-*-freebsd*/clang],         [PY_SUPPORT_TIER=3], dnl FreeBSD on AMD64
+  [aarch64-apple-ios*-macabi/clang],      [PY_SUPPORT_TIER=3], dnl MacCatalyst on arm64
+  [x86_64-apple-ios*-macabi/clang],       [PY_SUPPORT_TIER=3], dnl MacCatalyst on x86_64
   [aarch64-apple-ios*-simulator/clang],   [PY_SUPPORT_TIER=3], dnl iOS Simulator on arm64
   [aarch64-apple-ios*/clang],             [PY_SUPPORT_TIER=3], dnl iOS on ARM64
   [aarch64-apple-tvos*-simulator/clang],  [PY_SUPPORT_TIER=3], dnl tvOS Simulator on arm64
@@ -7932,13 +8007,17 @@ AS_CASE([$ac_sys_system],
     dnl curses and tkinter user interface are not available.
     dnl gdbm and nis aren't available
     dnl Stub implementations are provided for pwd, grp etc APIs
+    dnl subprocess is however supported for Mac Catalyst
+    case "$_host_device" in
+      macabi) ;;
+      *) PY_STDLIB_MOD_SET_NA([_posixsubprocess]) ;;
+    esac
     PY_STDLIB_MOD_SET_NA(
       [_curses],
       [_curses_panel],
       [_gdbm],
       [_multiprocessing],
       [_posixshmem],
-      [_posixsubprocess],
       [_scproxy],
       [_tkinter],
       [grp],
diff --git a/iOS/testbed/Python.xcframework/Info.plist b/iOS/testbed/Python.xcframework/Info.plist
index c6418de6e74..7b32df19ab4 100644
--- a/iOS/testbed/Python.xcframework/Info.plist
+++ b/iOS/testbed/Python.xcframework/Info.plist
@@ -4,6 +4,23 @@
 <dict>
 	<key>AvailableLibraries</key>
 	<array>
+		<dict>
+			<key>BinaryPath</key>
+			<string>Python.framework/Versions/Latest/Python</string>
+			<key>LibraryIdentifier</key>
+			<string>ios-arm64_x86_64-maccatalyst</string>
+			<key>LibraryPath</key>
+			<string>python.framework</string>
+			<key>SupportedArchitectures</key>
+			<array>
+				<string>x86_64</string>
+				<string>arm64</string>
+			</array>
+			<key>SupportedPlatform</key>
+			<string>ios</string>
+			<key>SupportedPlatformVariant</key>
+			<string>maccatalyst</string>
+		</dict>
 		<dict>
 			<key>BinaryPath</key>
 			<string>Python.framework/Python</string>
diff --git a/iOS/testbed/Python.xcframework/ios-arm64_x86_64-maccatalyst/README b/iOS/testbed/Python.xcframework/ios-arm64_x86_64-maccatalyst/README
new file mode 100644
index 00000000000..b39f40980bc
--- /dev/null
+++ b/iOS/testbed/Python.xcframework/ios-arm64_x86_64-maccatalyst/README
@@ -0,0 +1,4 @@
+This directory is intentionally empty.
+
+It is a placeholder slice for the XCFramework on Mac Catalyst,
+to install or copy your built framework to.
diff --git a/iOS/testbed/__main__.py b/iOS/testbed/__main__.py
index 1146bf3b988..1c42b07aebd 100644
--- a/iOS/testbed/__main__.py
+++ b/iOS/testbed/__main__.py
@@ -28,6 +28,15 @@
     r"\s+\(Python\)\s"  # Logger name
 )
 
+# Prefix: 2025-04-27 21:43:38.530606-0500 iOSTestbed[96892:48053672]
+CATALYST_LOG_PREFIX_REGEX = re.compile(
+    r"^\d{4}-\d{2}-\d{2}"        # YYYY-MM-DD
+    r"\s+\d{2}:\d{2}:\d{2}\.\d{6}" # HH:MM:SS.ssssss (microseconds, 6 digits)
+    r"[-+]\d{4}"                  # Timezone offset like -0500
+    r"\s+iOSTestbed\[\d+:\d+\] "    # iOSTestbed[ProcessID:ThreadID] (both numbers), then a space
+)
+
+
 
 # Work around a bug involving sys.exit and TaskGroups
 # (https://github.com/python/cpython/issues/101515).
@@ -243,9 +252,13 @@ async def log_stream_task(initial_devices, lock):
             sys.stdout.flush()
 
 
-async def xcode_test(location, simulator, verbose):
+async def xcode_test(location, simulator, verbose, catalyst):
     # Run the test suite on the named simulator
     print("Starting xcodebuild...", flush=True)
+    if catalyst:
+        destination_arg = "platform=macOS,variant=Mac Catalyst"
+    else:
+        destination_arg = f"platform=iOS Simulator,name={simulator}";
     args = [
         "xcodebuild",
         "test",
@@ -254,13 +267,13 @@ async def xcode_test(location, simulator, verbose):
         "-scheme",
         "iOSTestbed",
         "-destination",
-        f"platform=iOS Simulator,name={simulator}",
+        destination_arg,
         "-resultBundlePath",
         str(location / f"{datetime.now():%Y%m%d-%H%M%S}.xcresult"),
         "-derivedDataPath",
         str(location / "DerivedData"),
     ]
-    if not verbose:
+    if not verbose and not catalyst:
         args += ["-quiet"]
 
     async with async_process(
@@ -269,8 +282,16 @@ async def xcode_test(location, simulator, verbose):
         stderr=subprocess.STDOUT,
     ) as process:
         while line := (await process.stdout.readline()).decode(*DECODE_ARGS):
-            sys.stdout.write(line)
-            sys.stdout.flush()
+            # For Mac Catalyst, the *actual* logs are streamed here. Only stream
+            # things that does NOT come from the process when verbose.
+            if catalyst:
+                if CATALYST_LOG_PREFIX_REGEX.match(line) or verbose:
+                    line = CATALYST_LOG_PREFIX_REGEX.sub("", line)
+                    sys.stdout.write(line)
+                    sys.stdout.flush()
+            else:
+                sys.stdout.write(line)
+                sys.stdout.flush()
 
         status = await asyncio.wait_for(process.wait(), timeout=1)
         exit(status)
@@ -281,20 +302,31 @@ def clone_testbed(
     target: Path,
     framework: Path,
     apps: list[Path],
+    catalyst: bool,
 ) -> None:
     if target.exists():
         print(f"{target} already exists; aborting without creating project.")
         sys.exit(10)
 
     if framework is None:
-        if not (
-            source / "Python.xcframework/ios-arm64_x86_64-simulator/bin"
-        ).is_dir():
-            print(
-                f"The testbed being cloned ({source}) does not contain "
-                f"a simulator framework. Re-run with --framework"
-            )
-            sys.exit(11)
+        if catalyst:
+            if not (
+                source / "Python.xcframework/ios-arm64_x86_64-maccatalyst/Python.framework/Versions"
+            ).is_dir():
+                print(
+                    f"The testbed being cloned ({source}) does not contain "
+                    f"a Mac Catalyst framework. Re-run with --framework"
+                )
+                sys.exit(11)
+        else:
+            if not (
+                source / "Python.xcframework/ios-arm64_x86_64-simulator/bin"
+            ).is_dir():
+                print(
+                    f"The testbed being cloned ({source}) does not contain "
+                    f"a simulator framework. Re-run with --framework"
+                )
+                sys.exit(11)
     else:
         if not framework.is_dir():
             print(f"{framework} does not exist.")
@@ -305,7 +337,7 @@ def clone_testbed(
         ):
             print(
                 f"{framework} is not an XCframework, "
-                f"or a simulator slice of a framework build."
+                f"or a simulator / Catalyst slice of a framework build."
             )
             sys.exit(13)
 
@@ -315,7 +347,10 @@ def clone_testbed(
     print(" done")
 
     xc_framework_path = target / "Python.xcframework"
-    sim_framework_path = xc_framework_path / "ios-arm64_x86_64-simulator"
+    if catalyst:
+        sim_framework_path = xc_framework_path / "ios-arm64_x86_64-maccatalyst"
+    else:
+        sim_framework_path = xc_framework_path / "ios-arm64_x86_64-simulator"
     if framework is not None:
         if framework.suffix == ".xcframework":
             print("  Installing XCFramework...", end="", flush=True)
@@ -328,7 +363,7 @@ def clone_testbed(
             )
             print(" done")
         else:
-            print("  Installing simulator framework...", end="", flush=True)
+            print("  Installing simulator/catalyst framework...", end="", flush=True)
             if sim_framework_path.is_dir():
                 shutil.rmtree(sim_framework_path)
             else:
@@ -360,7 +395,7 @@ def clone_testbed(
             sim_framework_path.is_symlink()
             and not sim_framework_path.readlink().is_absolute()
         ):
-            print("  Rewriting symlink to simulator framework...", end="", flush=True)
+            print("  Rewriting symlink to simulator/catalyst framework...", end="", flush=True)
             # Simulator framework is a relative symlink. Rewrite the symlink
             # relative to the new location.
             orig_sim_framework_path = (
@@ -401,42 +436,52 @@ def update_plist(testbed_path, args):
         plistlib.dump(info, f)
 
 
-async def run_testbed(simulator: str | None, args: list[str], verbose: bool=False):
+async def run_testbed(simulator: str | None, args: list[str], catalyst: bool, verbose: bool=False):
     location = Path(__file__).parent
     print("Updating plist...", end="", flush=True)
     update_plist(location, args)
     print(" done.", flush=True)
 
-    if simulator is None:
-        simulator = await select_simulator_device()
-    print(f"Running test on {simulator}", flush=True)
-
-    # We need to get an exclusive lock on simulator creation, to avoid issues
-    # with multiple simulators starting and being unable to tell which
-    # simulator is due to which testbed instance. See
-    # https://github.com/python/cpython/issues/130294 for details. Wait up to
-    # 10 minutes for a simulator to boot.
-    print("Obtaining lock on simulator creation...", flush=True)
-    simulator_lock = SimulatorLock(timeout=10*60)
-    await simulator_lock.acquire()
-    print("Simulator lock acquired.", flush=True)
-
-    # Get the list of devices that are booted at the start of the test run.
-    # The simulator started by the test suite will be detected as the new
-    # entry that appears on the device list.
-    initial_devices = await list_devices()
-
-    try:
-        async with asyncio.TaskGroup() as tg:
-            tg.create_task(log_stream_task(initial_devices, simulator_lock))
-            tg.create_task(xcode_test(location, simulator=simulator, verbose=verbose))
-    except* MySystemExit as e:
-        raise SystemExit(*e.exceptions[0].args) from None
-    except* subprocess.CalledProcessError as e:
-        # Extract it from the ExceptionGroup so it can be handled by `main`.
-        raise e.exceptions[0]
-    finally:
-        simulator_lock.release()
+    if not catalyst:
+        if simulator is None:
+            simulator = await select_simulator_device()
+        print(f"Running test on {simulator}", flush=True)
+
+        # We need to get an exclusive lock on simulator creation, to avoid issues
+        # with multiple simulators starting and being unable to tell which
+        # simulator is due to which testbed instance. See
+        # https://github.com/python/cpython/issues/130294 for details. Wait up to
+        # 10 minutes for a simulator to boot.
+        print("Obtaining lock on simulator creation...", flush=True)
+        simulator_lock = SimulatorLock(timeout=10*60)
+        await simulator_lock.acquire()
+        print("Simulator lock acquired.", flush=True)
+
+        # Get the list of devices that are booted at the start of the test run.
+        # The simulator started by the test suite will be detected as the new
+        # entry that appears on the device list.
+        initial_devices = await list_devices()
+
+        try:
+            async with asyncio.TaskGroup() as tg:
+                tg.create_task(log_stream_task(initial_devices, simulator_lock))
+                tg.create_task(xcode_test(location, simulator=simulator, verbose=verbose, catalyst=False))
+        except* MySystemExit as e:
+            raise SystemExit(*e.exceptions[0].args) from None
+        except* subprocess.CalledProcessError as e:
+            # Extract it from the ExceptionGroup so it can be handled by `main`.
+            raise e.exceptions[0]
+        finally:
+            simulator_lock.release()
+    else:
+        try:
+            async with asyncio.TaskGroup() as tg:
+                tg.create_task(xcode_test(location, simulator="", verbose=verbose, catalyst=True))
+        except* MySystemExit as e:
+            raise SystemExit(*e.exceptions[0].args) from None
+        except* subprocess.CalledProcessError as e:
+            # Extract it from the ExceptionGroup so it can be handled by `main`.
+            raise e.exceptions[0]
 
 
 def main():
@@ -448,6 +493,12 @@ def main():
 
     subcommands = parser.add_subparsers(dest="subcommand")
 
+    parser.add_argument(
+        "--catalyst",
+        action="store_true",
+        help="Run or clone the testbed for a Mac Catalyst build.",
+    )
+
     clone = subcommands.add_parser(
         "clone",
         description=(
@@ -514,11 +565,16 @@ def main():
             target=Path(context.location).resolve(),
             framework=Path(context.framework).resolve() if context.framework else None,
             apps=[Path(app) for app in context.apps],
+            catalyst = context.catalyst
         )
     elif context.subcommand == "run":
         if test_args:
+            if context.catalyst:
+                expected_location = "Python.xcframework/ios-arm64_x86_64-maccatalyst/Python.framework"
+            else:
+                expected_location = "Python.xcframework/ios-arm64_x86_64-simulator/bin"
             if not (
-                Path(__file__).parent / "Python.xcframework/ios-arm64_x86_64-simulator/bin"
+                Path(__file__).parent / expected_location
             ).is_dir():
                 print(
                     f"Testbed does not contain a compiled iOS framework. Use "
@@ -532,6 +588,7 @@ def main():
                     simulator=context.simulator,
                     verbose=context.verbose,
                     args=test_args,
+                    catalyst=context.catalyst
                 )
             )
         else:
diff --git a/iOS/testbed/iOSTestbed.xcodeproj/project.pbxproj b/iOS/testbed/iOSTestbed.xcodeproj/project.pbxproj
index c7d63909ee2..ace08f7efd9 100644
--- a/iOS/testbed/iOSTestbed.xcodeproj/project.pbxproj
+++ b/iOS/testbed/iOSTestbed.xcodeproj/project.pbxproj
@@ -70,6 +70,7 @@
 		607A66592B0F08600010BFC8 /* iOSTestbed-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "iOSTestbed-Info.plist"; sourceTree = "<group>"; };
 		608619532CB77BA900F46182 /* app_packages */ = {isa = PBXFileReference; lastKnownFileType = folder; path = app_packages; sourceTree = "<group>"; };
 		608619552CB7819B00F46182 /* app */ = {isa = PBXFileReference; lastKnownFileType = folder; path = app; sourceTree = "<group>"; };
+		EE325B2A2DBE97CD000142D0 /* iOSTestbed.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = iOSTestbed.entitlements; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
 /* Begin PBXFrameworksBuildPhase section */
@@ -115,6 +116,7 @@
 		607A66142B0EFA380010BFC8 /* iOSTestbed */ = {
 			isa = PBXGroup;
 			children = (
+				EE325B2A2DBE97CD000142D0 /* iOSTestbed.entitlements */,
 				608619552CB7819B00F46182 /* app */,
 				608619532CB77BA900F46182 /* app_packages */,
 				607A66592B0F08600010BFC8 /* iOSTestbed-Info.plist */,
@@ -262,7 +264,7 @@
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 			shellPath = /bin/sh;
-			shellScript = "set -e\n\nmkdir -p \"$CODESIGNING_FOLDER_PATH/python/lib\"\nif [ \"$EFFECTIVE_PLATFORM_NAME\" = \"-iphonesimulator\" ]; then\n    echo \"Installing Python modules for iOS Simulator\"\n    rsync -au --delete \"$PROJECT_DIR/Python.xcframework/ios-arm64_x86_64-simulator/lib/\" \"$CODESIGNING_FOLDER_PATH/python/lib/\" \nelse\n    echo \"Installing Python modules for iOS Device\"\n    rsync -au --delete \"$PROJECT_DIR/Python.xcframework/ios-arm64/lib/\" \"$CODESIGNING_FOLDER_PATH/python/lib/\" \nfi\n";
+			shellScript = "set -e\n\nif [ \"$EFFECTIVE_PLATFORM_NAME\" = \"-iphonesimulator\" ]; then\n    mkdir -p \"$CODESIGNING_FOLDER_PATH/python/lib\"\n    echo \"Installing Python modules for iOS Simulator\"\n    rsync -au --delete \"$PROJECT_DIR/Python.xcframework/ios-arm64_x86_64-simulator/lib/\" \"$CODESIGNING_FOLDER_PATH/python/lib/\" \nelif [ \"$EFFECTIVE_PLATFORM_NAME\" = \"-maccatalyst\" ]; then\n    mkdir -p \"$CODESIGNING_FOLDER_PATH/Contents/Resources/python/lib\"\n    echo \"Installing Python modules for Mac Catalyst\"\n    rsync -au --delete \"$PROJECT_DIR/Python.xcframework/ios-arm64_x86_64-maccatalyst/Python.framework/Versions/3.14/lib/\" \"$CODESIGNING_FOLDER_PATH/Contents/Resources/python/lib/\"\nelse\n    mkdir -p \"$CODESIGNING_FOLDER_PATH/python/lib\"\n    echo \"Installing Python modules for iOS Device\"\n    rsync -au --delete \"$PROJECT_DIR/Python.xcframework/ios-arm64/lib/\" \"$CODESIGNING_FOLDER_PATH/python/lib/\"\nfi\n";
 			showEnvVarsInLog = 0;
 		};
 		607A66562B0F06200010BFC8 /* Prepare Python Binary Modules */ = {
@@ -282,7 +284,7 @@
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 			shellPath = /bin/sh;
-			shellScript = "set -e\n\ninstall_dylib () {\n    INSTALL_BASE=$1\n    FULL_EXT=$2\n\n    # The name of the extension file\n    EXT=$(basename \"$FULL_EXT\")\n    # The location of the extension file, relative to the bundle\n    RELATIVE_EXT=${FULL_EXT#$CODESIGNING_FOLDER_PATH/} \n    # The path to the extension file, relative to the install base\n    PYTHON_EXT=${RELATIVE_EXT/$INSTALL_BASE/}\n    # The full dotted name of the extension module, constructed from the file path.\n    FULL_MODULE_NAME=$(echo $PYTHON_EXT | cut -d \".\" -f 1 | tr \"/\" \".\"); \n    # A bundle identifier; not actually used, but required by Xcode framework packaging\n    FRAMEWORK_BUNDLE_ID=$(echo $PRODUCT_BUNDLE_IDENTIFIER.$FULL_MODULE_NAME | tr \"_\" \"-\")\n    # The name of the framework folder.\n    FRAMEWORK_FOLDER=\"Frameworks/$FULL_MODULE_NAME.framework\"\n\n    # If the framework folder doesn't exist, create it.\n    if [ ! -d \"$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER\" ]; then\n        echo \"Creating framework for $RELATIVE_EXT\" \n        mkdir -p \"$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER\"\n        cp \"$CODESIGNING_FOLDER_PATH/dylib-Info-template.plist\" \"$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/Info.plist\"\n        plutil -replace CFBundleExecutable -string \"$FULL_MODULE_NAME\" \"$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/Info.plist\"\n        plutil -replace CFBundleIdentifier -string \"$FRAMEWORK_BUNDLE_ID\" \"$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/Info.plist\"\n    fi\n    \n    echo \"Installing binary for $FRAMEWORK_FOLDER/$FULL_MODULE_NAME\" \n    mv \"$FULL_EXT\" \"$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/$FULL_MODULE_NAME\"\n    # Create a placeholder .fwork file where the .so was\n    echo \"$FRAMEWORK_FOLDER/$FULL_MODULE_NAME\" > ${FULL_EXT%.so}.fwork\n    # Create a back reference to the .so file location in the framework\n    echo \"${RELATIVE_EXT%.so}.fwork\" > \"$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/$FULL_MODULE_NAME.origin\"             \n}\n\nPYTHON_VER=$(ls -1 \"$CODESIGNING_FOLDER_PATH/python/lib\")\necho \"Install Python $PYTHON_VER standard library extension modules...\"\nfind \"$CODESIGNING_FOLDER_PATH/python/lib/$PYTHON_VER/lib-dynload\" -name \"*.so\" | while read FULL_EXT; do\n    install_dylib python/lib/$PYTHON_VER/lib-dynload/ \"$FULL_EXT\"\ndone\necho \"Install app package extension modules...\"\nfind \"$CODESIGNING_FOLDER_PATH/app_packages\" -name \"*.so\" | while read FULL_EXT; do\n    install_dylib app_packages/ \"$FULL_EXT\"\ndone\necho \"Install app extension modules...\"\nfind \"$CODESIGNING_FOLDER_PATH/app\" -name \"*.so\" | while read FULL_EXT; do\n    install_dylib app/ \"$FULL_EXT\"\ndone\n\n# Clean up dylib template \nrm -f \"$CODESIGNING_FOLDER_PATH/dylib-Info-template.plist\"\necho \"Signing frameworks as $EXPANDED_CODE_SIGN_IDENTITY_NAME ($EXPANDED_CODE_SIGN_IDENTITY)...\"\nfind \"$CODESIGNING_FOLDER_PATH/Frameworks\" -name \"*.framework\" -exec /usr/bin/codesign --force --sign \"$EXPANDED_CODE_SIGN_IDENTITY\" ${OTHER_CODE_SIGN_FLAGS:-} -o runtime --timestamp=none --preserve-metadata=identifier,entitlements,flags --generate-entitlement-der \"{}\" \\; \n";
+			shellScript = "set -e\n\ninstall_dylib () {\n    INSTALL_BASE=$1\n    FULL_EXT=$2\n\n    # The name of the extension file\n    EXT=$(basename \"$FULL_EXT\")\n    # The location of the extension file, relative to the bundle\n    RELATIVE_EXT=${FULL_EXT#$CODESIGNING_FOLDER_PATH/} \n    # The path to the extension file, relative to the install base\n    PYTHON_EXT=${RELATIVE_EXT/$INSTALL_BASE/}\n    # The full dotted name of the extension module, constructed from the file path.\n    FULL_MODULE_NAME=$(echo $PYTHON_EXT | cut -d \".\" -f 1 | tr \"/\" \".\"); \n    # A bundle identifier; not actually used, but required by Xcode framework packaging\n    FRAMEWORK_BUNDLE_ID=$(echo $PRODUCT_BUNDLE_IDENTIFIER.$FULL_MODULE_NAME | tr \"_\" \"-\")\n    # The name of the framework folder.\n    FRAMEWORK_FOLDER=\"Frameworks/$FULL_MODULE_NAME.framework\"\n\n    # If the framework folder doesn't exist, create it.\n    if [ ! -d \"$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER\" ]; then\n        echo \"Creating framework for $RELATIVE_EXT\" \n        mkdir -p \"$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER\"\n        cp \"$CODESIGNING_FOLDER_PATH/dylib-Info-template.plist\" \"$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/Info.plist\"\n        plutil -replace CFBundleExecutable -string \"$FULL_MODULE_NAME\" \"$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/Info.plist\"\n        plutil -replace CFBundleIdentifier -string \"$FRAMEWORK_BUNDLE_ID\" \"$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/Info.plist\"\n    fi\n    \n    echo \"Installing binary for $FRAMEWORK_FOLDER/$FULL_MODULE_NAME\" \n    mv \"$FULL_EXT\" \"$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/$FULL_MODULE_NAME\"\n    # Create a placeholder .fwork file where the .so was\n    echo \"$FRAMEWORK_FOLDER/$FULL_MODULE_NAME\" > ${FULL_EXT%.so}.fwork\n    # Create a back reference to the .so file location in the framework\n    echo \"${RELATIVE_EXT%.so}.fwork\" > \"$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/$FULL_MODULE_NAME.origin\"             \n}\n\nif [ \"$EFFECTIVE_PLATFORM_NAME\" = \"-maccatalyst\" ]; then\n    PYTHON_VER=$(ls -1 \"$CODESIGNING_FOLDER_PATH/Contents/Resources/python/lib\")\n    find \"$CODESIGNING_FOLDER_PATH/Contents/Resources/python/lib/$PYTHON_VER/lib-dynload\" -name \"*.so\" -exec /usr/bin/codesign --force --sign \"$EXPANDED_CODE_SIGN_IDENTITY\" -o runtime --timestamp=none --preserve-metadata=identifier,entitlements,flags --generate-entitlement-der {} \\;\n    find \"$CODESIGNING_FOLDER_PATH/Contents/Resources/app_packages\" -name \"*.so\" -exec /usr/bin/codesign --force --sign \"$EXPANDED_CODE_SIGN_IDENTITY\" -o runtime --timestamp=none --preserve-metadata=identifier,entitlements,flags --generate-entitlement-der {} \\;\n    find \"$CODESIGNING_FOLDER_PATH/Contents/Resources/app\" -name \"*.so\" -exec /usr/bin/codesign --force --sign \"$EXPANDED_CODE_SIGN_IDENTITY\" -o runtime --timestamp=none --preserve-metadata=identifier,entitlements,flags --generate-entitlement-der {} \\;\n    \nelse\n    PYTHON_VER=$(ls -1 \"$CODESIGNING_FOLDER_PATH/python/lib\")\n    echo \"Install Python $PYTHON_VER standard library extension modules...\"\n    find \"$CODESIGNING_FOLDER_PATH/python/lib/$PYTHON_VER/lib-dynload\" -name \"*.so\" | while read FULL_EXT; do\n        install_dylib python/lib/$PYTHON_VER/lib-dynload/ \"$FULL_EXT\"\n    done\n    echo \"Install app package extension modules...\"\n    find \"$CODESIGNING_FOLDER_PATH/app_packages\" -name \"*.so\" | while read FULL_EXT; do\n        install_dylib app_packages/ \"$FULL_EXT\"\n    done\n    echo \"Install app extension modules...\"\n    find \"$CODESIGNING_FOLDER_PATH/app\" -name \"*.so\" | while read FULL_EXT; do\n        install_dylib app/ \"$FULL_EXT\"\n    done\n\n    # Clean up dylib template \n    rm -f \"$CODESIGNING_FOLDER_PATH/dylib-Info-template.plist\"\n    echo \"Signing frameworks as $EXPANDED_CODE_SIGN_IDENTITY_NAME ($EXPANDED_CODE_SIGN_IDENTITY)...\"\n    find \"$CODESIGNING_FOLDER_PATH/Frameworks\" -name \"*.framework\" -exec /usr/bin/codesign --force --sign \"$EXPANDED_CODE_SIGN_IDENTITY\" ${OTHER_CODE_SIGN_FLAGS:-} -o runtime --timestamp=none --preserve-metadata=identifier,entitlements,flags --generate-entitlement-der \"{}\" \\; \nfi\n";
 			showEnvVarsInLog = 0;
 		};
 /* End PBXShellScriptBuildPhase section */
@@ -362,6 +364,7 @@
 				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
 				COPY_PHASE_STRIP = NO;
 				DEBUG_INFORMATION_FORMAT = dwarf;
+				ENABLE_HARDENED_RUNTIME = YES;
 				ENABLE_STRICT_OBJC_MSGSEND = YES;
 				ENABLE_TESTABILITY = YES;
 				ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -381,6 +384,7 @@
 				GCC_WARN_UNUSED_VARIABLE = YES;
 				IPHONEOS_DEPLOYMENT_TARGET = 12.0;
 				LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+				MACOSX_DEPLOYMENT_TARGET = 10.14;
 				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
 				MTL_FAST_MATH = YES;
 				ONLY_ACTIVE_ARCH = YES;
@@ -423,6 +427,7 @@
 				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
 				COPY_PHASE_STRIP = NO;
 				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+				ENABLE_HARDENED_RUNTIME = YES;
 				ENABLE_NS_ASSERTIONS = NO;
 				ENABLE_STRICT_OBJC_MSGSEND = YES;
 				ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -436,6 +441,7 @@
 				GCC_WARN_UNUSED_VARIABLE = YES;
 				IPHONEOS_DEPLOYMENT_TARGET = 12.0;
 				LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+				MACOSX_DEPLOYMENT_TARGET = 10.14;
 				MTL_ENABLE_DEBUG_INFO = NO;
 				MTL_FAST_MATH = YES;
 				SDKROOT = iphoneos;
@@ -449,9 +455,12 @@
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
 				CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO;
+				CODE_SIGN_ENTITLEMENTS = iOSTestbed/iOSTestbed.entitlements;
+				"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
 				CODE_SIGN_STYLE = Automatic;
 				CURRENT_PROJECT_VERSION = 1;
 				DEVELOPMENT_TEAM = "";
+				ENABLE_HARDENED_RUNTIME = YES;
 				ENABLE_USER_SCRIPT_SANDBOXING = NO;
 				HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/Python.framework/Headers\"";
 				INFOPLIST_FILE = "iOSTestbed/iOSTestbed-Info.plist";
@@ -468,6 +477,9 @@
 				MARKETING_VERSION = 3.13.0a1;
 				PRODUCT_BUNDLE_IDENTIFIER = org.python.iOSTestbed;
 				PRODUCT_NAME = "$(TARGET_NAME)";
+				PROVISIONING_PROFILE_SPECIFIER = "";
+				SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
+				SUPPORTS_MACCATALYST = YES;
 				SWIFT_EMIT_LOC_STRINGS = YES;
 				TARGETED_DEVICE_FAMILY = "1,2";
 			};
@@ -479,9 +491,12 @@
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
 				CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO;
+				CODE_SIGN_ENTITLEMENTS = iOSTestbed/iOSTestbed.entitlements;
+				"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
 				CODE_SIGN_STYLE = Automatic;
 				CURRENT_PROJECT_VERSION = 1;
 				DEVELOPMENT_TEAM = "";
+				ENABLE_HARDENED_RUNTIME = YES;
 				ENABLE_TESTABILITY = YES;
 				ENABLE_USER_SCRIPT_SANDBOXING = NO;
 				HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/Python.framework/Headers\"";
@@ -499,6 +514,9 @@
 				MARKETING_VERSION = 3.13.0a1;
 				PRODUCT_BUNDLE_IDENTIFIER = org.python.iOSTestbed;
 				PRODUCT_NAME = "$(TARGET_NAME)";
+				PROVISIONING_PROFILE_SPECIFIER = "";
+				SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
+				SUPPORTS_MACCATALYST = YES;
 				SWIFT_EMIT_LOC_STRINGS = YES;
 				TARGETED_DEVICE_FAMILY = "1,2";
 			};
@@ -509,9 +527,10 @@
 			buildSettings = {
 				BUNDLE_LOADER = "$(TEST_HOST)";
 				CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO;
+				"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
 				CODE_SIGN_STYLE = Automatic;
 				CURRENT_PROJECT_VERSION = 1;
-				DEVELOPMENT_TEAM = 3HEZE76D99;
+				DEVELOPMENT_TEAM = "";
 				GENERATE_INFOPLIST_FILE = YES;
 				HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/Python.framework/Headers\"";
 				IPHONEOS_DEPLOYMENT_TARGET = 12.0;
@@ -529,9 +548,10 @@
 			buildSettings = {
 				BUNDLE_LOADER = "$(TEST_HOST)";
 				CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO;
+				"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
 				CODE_SIGN_STYLE = Automatic;
 				CURRENT_PROJECT_VERSION = 1;
-				DEVELOPMENT_TEAM = 3HEZE76D99;
+				DEVELOPMENT_TEAM = "";
 				GENERATE_INFOPLIST_FILE = YES;
 				HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/Python.framework/Headers\"";
 				IPHONEOS_DEPLOYMENT_TARGET = 12.0;
diff --git a/iOS/testbed/iOSTestbed/iOSTestbed.entitlements b/iOS/testbed/iOSTestbed/iOSTestbed.entitlements
new file mode 100644
index 00000000000..8cc185af8d7
--- /dev/null
+++ b/iOS/testbed/iOSTestbed/iOSTestbed.entitlements
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>com.apple.security.cs.disable-library-validation</key>
+	<true/>
+</dict>
+</plist>

@johnzhou721 johnzhou721 closed this Oct 4, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants